From 82bd13512f5bd36cb4f4bc6e9b414044e4618611 Mon Sep 17 00:00:00 2001 From: Habib Alkhabbaz <31035020+habibalkhabbaz@users.noreply.github.com> Date: Fri, 22 Jul 2022 15:57:50 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20feat:=20use=20websocket=20for=20?= =?UTF-8?q?candles,=20orders=20and=20account=20balances=20updates=20(#431)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 + app/__tests__/server-binance.test.js | 997 +++++++----- app/__tests__/server-cronjob.test.js | 80 +- app/binance/__tests__/ath-candles.test.js | 365 +++++ app/binance/__tests__/candles.test.js | 227 +++ app/binance/__tests__/orders.test.js | 241 +++ app/binance/__tests__/tickers.test.js | 178 +++ app/binance/__tests__/user.test.js | 572 +++++++ app/binance/ath-candles.js | 144 ++ app/binance/candles.js | 118 ++ app/binance/orders.js | 86 ++ app/binance/tickers.js | 88 ++ app/binance/user.js | 107 ++ app/cronjob/__tests__/trailingTrade.test.js | 393 ++--- .../__tests__/trailingTradeIndicator.test.js | 34 - app/cronjob/trailingTrade.js | 276 ++-- .../step/__tests__/cancel-order.test.js | 12 +- .../ensure-grid-trade-order-executed.test.js | 1368 ++--------------- .../__tests__/ensure-manual-order.test.js | 545 +------ .../step/__tests__/get-balances.test.js | 6 +- .../step/__tests__/get-indicators.test.js | 622 ++++++-- .../step/__tests__/handle-open-orders.test.js | 730 +++++---- .../step/__tests__/place-buy-order.test.js | 386 +++-- .../step/__tests__/place-manual-trade.test.js | 62 +- .../step/__tests__/place-sell-order.test.js | 379 ++--- .../place-sell-stop-loss-order.test.js | 258 ++-- .../__tests__/remove-last-buy-price.test.js | 56 +- .../step/__tests__/save-data-to-cache.test.js | 51 +- .../trailingTrade/step/cancel-order.js | 8 +- .../trailingTrade/step/determine-action.js | 4 +- .../step/ensure-grid-trade-order-executed.js | 387 +---- .../trailingTrade/step/ensure-manual-order.js | 170 +- .../trailingTrade/step/get-balances.js | 2 +- .../trailingTrade/step/get-indicators.js | 137 +- .../trailingTrade/step/handle-open-orders.js | 61 +- .../trailingTrade/step/place-buy-order.js | 47 +- .../trailingTrade/step/place-manual-trade.js | 27 +- .../trailingTrade/step/place-sell-order.js | 22 +- .../step/place-sell-stop-loss-order.js | 10 +- .../step/remove-last-buy-price.js | 6 +- .../trailingTrade/step/save-data-to-cache.js | 19 +- .../__tests__/common.test.js | 516 +++++++ .../__tests__/configuration.test.js | 481 +++++- .../__tests__/order.test.js | 70 + app/cronjob/trailingTradeHelper/common.js | 280 +++- .../trailingTradeHelper/configuration.js | 47 +- app/cronjob/trailingTradeHelper/order.js | 46 +- app/cronjob/trailingTradeIndicator.js | 21 - .../step/__tests__/get-account-info.test.js | 24 +- .../step/__tests__/save-data-to-cache.test.js | 10 - .../step/get-account-info.js | 6 +- .../step/save-data-to-cache.js | 10 +- .../__tests__/grid-trade-logs-export.test.js | 16 +- .../handlers/grid-trade-logs-export.js | 4 +- app/frontend/websocket/configure.js | 6 +- .../handlers/__tests__/cancel-order.test.js | 13 + .../__tests__/exchange-symbols-get.test.js | 81 + .../fixtures/latest-stats-authenticated.json | 688 +++------ ...t-stats-not-authenticated-unlock-list.json | 688 +++------ .../latest-trailing-trade-symbols.json | 1186 +++++++++++++- .../latest-trailing-trade-tradingview.json | 3 + .../handlers/__tests__/index.test.js | 3 +- .../handlers/__tests__/latest.test.js | 217 ++- .../manual-trade-all-symbols.test.js | 185 ++- .../handlers/__tests__/manual-trade.test.js | 14 + .../__tests__/symbol-enable-action.test.js | 14 + .../symbol-grid-trade-delete.test.js | 36 + .../__tests__/symbol-setting-delete.test.js | 15 + .../__tests__/symbol-setting-update.test.js | 14 + .../__tests__/symbol-trigger-buy.test.js | 14 + .../__tests__/symbol-trigger-sell.test.js | 14 + .../symbol-update-last-buy-price.test.js | 28 + .../websocket/handlers/cancel-order.js | 3 + .../handlers/exchange-symbols-get.js | 18 + app/frontend/websocket/handlers/index.js | 4 +- app/frontend/websocket/handlers/latest.js | 125 +- .../handlers/manual-trade-all-symbols.js | 13 +- .../websocket/handlers/manual-trade.js | 3 + .../websocket/handlers/symbol-delete.js | 2 + .../handlers/symbol-enable-action.js | 3 + .../handlers/symbol-grid-trade-delete.js | 5 +- .../handlers/symbol-setting-delete.js | 3 + .../handlers/symbol-setting-update.js | 3 + .../websocket/handlers/symbol-trigger-buy.js | 3 + .../websocket/handlers/symbol-trigger-sell.js | 3 + .../handlers/symbol-update-last-buy-price.js | 5 + app/helpers/__tests__/binance.test.js | 3 +- app/helpers/binance.js | 1 + app/server-binance.js | 284 ++-- app/server-cronjob.js | 8 +- .../1654430019999-create-candles-index.js | 36 + .../1655317029674-create-cache-index.js | 32 + public/css/App.css | 77 +- public/js/AccountWrapper.js | 29 +- public/js/AccountWrapperAsset.js | 49 +- public/js/App.js | 173 ++- public/js/CoinWrapperAction.js | 32 +- public/js/FilterIcon.js | 14 +- public/js/ProfitLossWrapper.js | 96 +- public/js/SettingIcon.js | 88 +- public/js/SettingIconBotOptions.js | 4 +- public/js/Status.js | 17 +- public/js/SymbolSettingIconBotOptions.js | 4 +- 103 files changed, 9493 insertions(+), 5686 deletions(-) create mode 100644 app/binance/__tests__/ath-candles.test.js create mode 100644 app/binance/__tests__/candles.test.js create mode 100644 app/binance/__tests__/orders.test.js create mode 100644 app/binance/__tests__/tickers.test.js create mode 100644 app/binance/__tests__/user.test.js create mode 100644 app/binance/ath-candles.js create mode 100644 app/binance/candles.js create mode 100644 app/binance/orders.js create mode 100644 app/binance/tickers.js create mode 100644 app/binance/user.js create mode 100644 app/frontend/websocket/handlers/__tests__/exchange-symbols-get.test.js create mode 100644 app/frontend/websocket/handlers/__tests__/fixtures/latest-trailing-trade-tradingview.json create mode 100644 app/frontend/websocket/handlers/exchange-symbols-get.js create mode 100644 migrations/1654430019999-create-candles-index.js create mode 100644 migrations/1655317029674-create-cache-index.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 4665561f..c49df555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +- Enhanced to use WebSocket for monitoring candles/orders/account information. It's faster! - [#431](https://github.com/chrisleekr/binance-trading-bot/pull/431) +- Updated the frontend with pagination [#431](https://github.com/chrisleekr/binance-trading-bot/pull/431) +- Updated account balance layout in the frontend [#431](https://github.com/chrisleekr/binance-trading-bot/pull/431) + +Thanks [@habibalkhabbaz](https://github.com/habibalkhabbaz) for all these updates! + ## [0.0.85] - 2021-11-02 - Refactored TradingView python server - [#383](https://github.com/chrisleekr/binance-trading-bot/pull/383) diff --git a/app/__tests__/server-binance.test.js b/app/__tests__/server-binance.test.js index fa603c5b..4199a17f 100644 --- a/app/__tests__/server-binance.test.js +++ b/app/__tests__/server-binance.test.js @@ -1,37 +1,62 @@ /* eslint-disable global-require */ - +// eslint-disable-next-line max-classes-per-file describe('server-binance', () => { - let config; - let PubSubMock; - let binanceMock; let loggerMock; let cacheMock; - let slackMock; + let mongoMock; let mockGetGlobalConfiguration; - let mockWebsocketCandlesClean; - let mockGetAccountInfo; + let mockGetAccountInfoFromAPI; + let mockLockSymbol; + let mockUnlockSymbol; + let mockCacheExchangeSymbols; + + let mockSetupUserWebsocket; + + let mockSyncCandles; + let mockSetupCandlesWebsocket; + let mockGetWebsocketCandlesClean; + + let mockSyncATHCandles; + let mockSetupATHCandlesWebsocket; + let mockGetWebsocketATHCandlesClean; + + let mockSetupTickersWebsocket; + let mockRefreshTickersClean; + let mockGetWebsocketTickersClean; + + let mockSyncOpenOrders; + let mockSyncDatabaseOrders; + + let mockGetAPILimit; + let mockSlack; + let config; beforeEach(async () => { jest.clearAllMocks().resetModules(); jest.useFakeTimers(); + + const { PubSub, logger, cache, mongo, slack } = require('../helpers'); + jest.mock('config'); - const { PubSub, binance, logger, cache, slack } = require('../helpers'); + config = require('config'); PubSubMock = PubSub; - binanceMock = binance; loggerMock = logger; cacheMock = cache; - slackMock = slack; + mongoMock = mongo; - config = require('config'); + mockSlack = slack; + mockSlack.sendMessage = jest.fn().mockResolvedValue(true); + + mockGetAPILimit = jest.fn().mockReturnValue(10); }); describe('when the bot is running live mode', () => { - describe('when websocket candles clean is null', () => { + describe('when the bot just started', () => { beforeEach(async () => { config.get = jest.fn(key => { switch (key) { @@ -42,36 +67,77 @@ describe('server-binance', () => { } }); + mockLockSymbol = jest.fn().mockResolvedValue(true); + mockUnlockSymbol = jest.fn().mockResolvedValue(true); + + mockSetupUserWebsocket = jest.fn().mockResolvedValue(true); + + mockSyncCandles = jest.fn().mockResolvedValue(true); + mockSetupCandlesWebsocket = jest.fn().mockResolvedValue(true); + mockGetWebsocketCandlesClean = jest.fn().mockResolvedValue(1); + + mockSyncATHCandles = jest.fn().mockResolvedValue(true); + mockSetupATHCandlesWebsocket = jest.fn().mockResolvedValue(true); + mockGetWebsocketATHCandlesClean = jest.fn().mockResolvedValue(1); + + mockSetupTickersWebsocket = jest.fn().mockResolvedValue(true); + mockRefreshTickersClean = jest.fn().mockResolvedValue(true); + mockGetWebsocketTickersClean = jest.fn().mockResolvedValue(1); + + mockSyncOpenOrders = jest.fn().mockResolvedValue(true); + mockSyncDatabaseOrders = jest.fn().mockResolvedValue(true); + mockGetGlobalConfiguration = jest.fn().mockResolvedValue({ symbols: ['BTCUSDT', 'BNBUSDT'] }); - mockGetAccountInfo = jest.fn().mockResolvedValue({ - balances: [ - { - asset: 'BTC' - }, - { - asset: 'BNB' - }, - { - asset: 'ETH' - }, - { - asset: 'USDT' - } - ] + mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + account: 'info' }); + mockCacheExchangeSymbols = jest.fn().mockResolvedValue(true); + jest.mock('../cronjob/trailingTradeHelper/configuration', () => ({ getGlobalConfiguration: mockGetGlobalConfiguration })); jest.mock('../cronjob/trailingTradeHelper/common', () => ({ - getAccountInfo: mockGetAccountInfo + getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + lockSymbol: mockLockSymbol, + unlockSymbol: mockUnlockSymbol, + cacheExchangeSymbols: mockCacheExchangeSymbols, + getAPILimit: mockGetAPILimit + })); + + jest.mock('../binance/user', () => ({ + setupUserWebsocket: mockSetupUserWebsocket + })); + + jest.mock('../binance/orders', () => ({ + syncOpenOrders: mockSyncOpenOrders, + syncDatabaseOrders: mockSyncDatabaseOrders + })); + + jest.mock('../binance/candles', () => ({ + syncCandles: mockSyncCandles, + setupCandlesWebsocket: mockSetupCandlesWebsocket, + getWebsocketCandlesClean: mockGetWebsocketCandlesClean })); - mockWebsocketCandlesClean = jest.fn().mockResolvedValue(true); + jest.mock('../binance/ath-candles', () => ({ + syncATHCandles: mockSyncATHCandles, + setupATHCandlesWebsocket: mockSetupATHCandlesWebsocket, + getWebsocketATHCandlesClean: mockGetWebsocketATHCandlesClean + })); + + jest.mock('../binance/tickers', () => ({ + setupTickersWebsocket: mockSetupTickersWebsocket, + refreshTickersClean: mockRefreshTickersClean, + getWebsocketTickersClean: mockGetWebsocketTickersClean + })); + + mongoMock.deleteAll = jest.fn().mockResolvedValue(true); + PubSubMock.subscribe = jest.fn().mockImplementation((_key, cb) => { cb('message', 'data'); }); @@ -83,32 +149,20 @@ describe('server-binance', () => { ); cacheMock.hset = jest.fn().mockResolvedValue(true); - binanceMock.client.ws.candles = jest - .fn() - .mockImplementation((_symbols, _interval, cb) => { - cb({ - symbol: 'BTCUSDT' - }); - cb({ - symbol: 'BNBBTC' - }); - }); - const { runBinance } = require('../server-binance'); await runBinance(loggerMock); }); - it('triggers binanceMock.client.ws.candles', () => { - expect(binanceMock.client.ws.candles).toHaveBeenCalledWith( - ['BTCUSDT', 'BNBUSDT', 'BNBBTC', 'ETHBTC'], - '1m', + it('triggers PubSub.subscribe for reset-all-websockets', () => { + expect(PubSubMock.subscribe).toHaveBeenCalledWith( + 'reset-all-websockets', expect.any(Function) ); }); - it('triggers PubSub.subscribe', () => { + it('triggers PubSub.subscribe for reset-symbol-websockets', () => { expect(PubSubMock.subscribe).toHaveBeenCalledWith( - 'reset-binance-websocket', + 'reset-symbol-websockets', expect.any(Function) ); }); @@ -117,119 +171,58 @@ describe('server-binance', () => { expect(mockGetGlobalConfiguration).toHaveBeenCalled(); }); - it('does not trigger websocketCandlesClean', () => { - expect(mockWebsocketCandlesClean).not.toHaveBeenCalled(); + it('triggers getAccountInfoFromAPI', () => { + expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); }); - ['BTCUSDT', 'BNBBTC'].forEach(symbol => { - it('triggers cache.hset', () => { - expect(cacheMock.hset).toHaveBeenCalledWith( - 'trailing-trade-symbols', - `${symbol}-latest-candle`, - JSON.stringify({ symbol }) - ); - }); - }); - }); - - describe('when exchange symbols are not cached', () => { - beforeEach(async () => { - config.get = jest.fn(key => { - switch (key) { - case 'mode': - return 'live'; - default: - return `value-${key}`; - } - }); - - mockGetGlobalConfiguration = jest.fn().mockResolvedValue({ - symbols: ['BTCUSDT', 'BNBUSDT'] - }); - - mockGetAccountInfo = jest.fn().mockResolvedValue({ - balances: [ - { - asset: 'BTC' - }, - { - asset: 'BNB' - }, - { - asset: 'ETH' - }, - { - asset: 'USDT' - } - ] - }); - - jest.mock('../cronjob/trailingTradeHelper/configuration', () => ({ - getGlobalConfiguration: mockGetGlobalConfiguration - })); - - jest.mock('../cronjob/trailingTradeHelper/common', () => ({ - getAccountInfo: mockGetAccountInfo - })); - - mockWebsocketCandlesClean = jest.fn().mockResolvedValue(true); - PubSubMock.subscribe = jest.fn().mockImplementation((_key, cb) => { - cb('message', 'data'); - }); - - cacheMock.hget = jest.fn().mockResolvedValue(null); - cacheMock.hset = jest.fn().mockResolvedValue(true); - - binanceMock.client.ws.candles = jest - .fn() - .mockImplementation((_symbols, _interval, cb) => { - cb({ - symbol: 'BTCUSDT' - }); - cb({ - symbol: 'BNBBTC' - }); - }); - - const { runBinance } = require('../server-binance'); - await runBinance(loggerMock); + it('triggers refreshCandles', () => { + expect(mongoMock.deleteAll).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-candles', + {} + ); + expect(mongoMock.deleteAll).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-ath-candles', + {} + ); }); - it('triggers binanceMock.client.ws.candles', () => { - expect(binanceMock.client.ws.candles).toHaveBeenCalledWith( - ['BTCUSDT', 'BNBUSDT'], - '1m', - expect.any(Function) - ); + it('triggers syncCandles', () => { + expect(mockSyncCandles).toHaveBeenCalledWith(loggerMock, [ + 'BTCUSDT', + 'BNBUSDT' + ]); }); - it('triggers PubSub.subscribe', () => { - expect(PubSubMock.subscribe).toHaveBeenCalledWith( - 'reset-binance-websocket', - expect.any(Function) - ); + it('triggers syncATHCandles', () => { + expect(mockSyncATHCandles).toHaveBeenCalledWith(loggerMock, [ + 'BTCUSDT', + 'BNBUSDT' + ]); }); - it('triggers getGlobalConfiguration', () => { - expect(mockGetGlobalConfiguration).toHaveBeenCalled(); + it('triggers syncOpenOrders', () => { + expect(mockSyncOpenOrders).toHaveBeenCalledWith(loggerMock, [ + 'BTCUSDT', + 'BNBUSDT' + ]); }); - it('does not trigger websocketCandlesClean', () => { - expect(mockWebsocketCandlesClean).not.toHaveBeenCalled(); + it('triggers syncDatabaseOrders', () => { + expect(mockSyncDatabaseOrders).toHaveBeenCalledWith(loggerMock); }); - ['BTCUSDT', 'BNBBTC'].forEach(symbol => { - it('triggers cache.hset', () => { - expect(cacheMock.hset).toHaveBeenCalledWith( - 'trailing-trade-symbols', - `${symbol}-latest-candle`, - JSON.stringify({ symbol }) - ); - }); + it('triggers cache.hset', () => { + expect(cacheMock.hset).toHaveBeenCalledWith( + 'trailing-trade-streams', + `count`, + 1 + ); }); }); - describe('when websocket candles clean is not null', () => { + describe('calculates number of open streams', () => { beforeEach(async () => { config.get = jest.fn(key => { switch (key) { @@ -240,290 +233,296 @@ describe('server-binance', () => { } }); + + mockLockSymbol = jest.fn().mockResolvedValue(true); + mockUnlockSymbol = jest.fn().mockResolvedValue(true); + + mockSetupUserWebsocket = jest.fn().mockResolvedValue(true); + + mockSyncCandles = jest.fn().mockResolvedValue(true); + mockSetupCandlesWebsocket = jest.fn().mockResolvedValue(true); + mockGetWebsocketCandlesClean = jest + .fn() + .mockImplementation(() => ({ '1h': () => true })); + + mockSyncATHCandles = jest.fn().mockResolvedValue(true); + mockSetupATHCandlesWebsocket = jest.fn().mockResolvedValue(true); + + mockGetWebsocketATHCandlesClean = jest + .fn() + .mockImplementation(() => ({ '1d': () => true, '30m': () => true })); + + mockSetupTickersWebsocket = jest.fn().mockResolvedValue(true); + mockRefreshTickersClean = jest.fn().mockResolvedValue(true); + mockGetWebsocketTickersClean = jest.fn().mockImplementation(() => ({ + BTCUSDT: () => true, + BNBUSDT: () => true + })); + + mockSyncOpenOrders = jest.fn().mockResolvedValue(true); + mockSyncDatabaseOrders = jest.fn().mockResolvedValue(true); + mockGetGlobalConfiguration = jest.fn().mockResolvedValue({ symbols: ['BTCUSDT', 'BNBUSDT'] }); - mockGetAccountInfo = jest.fn().mockResolvedValue({ - balances: [ - { - asset: 'BTC' - }, - { - asset: 'BNB' - }, - { - asset: 'ETH' - }, - { - asset: 'USDT' - } - ] + mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + account: 'info' }); + mockCacheExchangeSymbols = jest.fn().mockResolvedValue(true); + jest.mock('../cronjob/trailingTradeHelper/configuration', () => ({ getGlobalConfiguration: mockGetGlobalConfiguration })); jest.mock('../cronjob/trailingTradeHelper/common', () => ({ - getAccountInfo: mockGetAccountInfo + getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + lockSymbol: mockLockSymbol, + unlockSymbol: mockUnlockSymbol, + cacheExchangeSymbols: mockCacheExchangeSymbols })); - mockWebsocketCandlesClean = jest.fn().mockResolvedValue(true); - PubSubMock.subscribe = jest.fn().mockImplementation((_key, cb) => { - cb('message', 'data'); - }); + jest.mock('../binance/user', () => ({ + setupUserWebsocket: mockSetupUserWebsocket + })); - cacheMock.hget = jest - .fn() - .mockResolvedValue( - JSON.stringify(require('./fixtures/exchange-symbols.json')) - ); - cacheMock.hset = jest.fn().mockResolvedValue(true); + jest.mock('../binance/orders', () => ({ + syncOpenOrders: mockSyncOpenOrders, + syncDatabaseOrders: mockSyncDatabaseOrders + })); - binanceMock.client.ws.candles = jest - .fn() - .mockImplementation((_symbols, _interval, cb) => { - cb({ - symbol: 'BTCUSDT' - }); + jest.mock('../binance/candles', () => ({ + syncCandles: mockSyncCandles, + setupCandlesWebsocket: mockSetupCandlesWebsocket, + getWebsocketCandlesClean: mockGetWebsocketCandlesClean + })); + + jest.mock('../binance/ath-candles', () => ({ + syncATHCandles: mockSyncATHCandles, + setupATHCandlesWebsocket: mockSetupATHCandlesWebsocket, + getWebsocketATHCandlesClean: mockGetWebsocketATHCandlesClean + })); - cb({ - symbol: 'BNBBTC' - }); + jest.mock('../binance/tickers', () => ({ + setupTickersWebsocket: mockSetupTickersWebsocket, + refreshTickersClean: mockRefreshTickersClean, + getWebsocketTickersClean: mockGetWebsocketTickersClean + })); - return mockWebsocketCandlesClean; - }); + PubSubMock.subscribe = jest.fn().mockResolvedValue(true); + + mongoMock.deleteAll = jest.fn().mockResolvedValue(true); + + cacheMock.hset = jest.fn().mockResolvedValue(true); const { runBinance } = require('../server-binance'); await runBinance(loggerMock); + }); - await runBinance(loggerMock); + it('triggers getWebsocketTickersClean', () => { + expect(mockGetWebsocketTickersClean).toHaveBeenCalled(); }); - it('triggers PubSub.subscribe', () => { - expect(PubSubMock.subscribe).toHaveBeenCalledWith( - 'reset-binance-websocket', - expect.any(Function) - ); + it('triggers getWebsocketATHCandlesClean', () => { + expect(mockGetWebsocketATHCandlesClean).toHaveBeenCalled(); }); - it('triggers getGlobalConfiguration', () => { - expect(mockGetGlobalConfiguration).toHaveBeenCalled(); + it('triggers getWebsocketCandlesClean', () => { + expect(mockGetWebsocketCandlesClean).toHaveBeenCalled(); }); - it('triggers websocketCandlesClean', () => { - expect(mockWebsocketCandlesClean).toHaveBeenCalled(); + it('triggers cacheExchangeSymbols', () => { + expect(mockCacheExchangeSymbols).toHaveBeenCalled(); }); it('triggers cache.hset', () => { expect(cacheMock.hset).toHaveBeenCalledWith( - 'trailing-trade-symbols', - 'BTCUSDT-latest-candle', - JSON.stringify({ symbol: 'BTCUSDT' }) + 'trailing-trade-streams', + `count`, + 1 + 5 ); }); }); + }); - describe('when lastReceivedAt passed timeout', () => { - describe('when notifyDebug is on', () => { - let dateNow = new Date('2021-05-07T00:00:00Z').valueOf(); - beforeEach(async () => { - config.get = jest.fn(key => { - switch (key) { - case 'mode': - return 'live'; - case 'featureToggle.notifyDebug': - return true; - default: - return `value-${key}`; - } - }); + describe('with errors', () => { + beforeEach(async () => { + mockLockSymbol = jest.fn().mockResolvedValue(true); + mockUnlockSymbol = jest.fn().mockResolvedValue(true); - // Mock Date.now for manipulating moment.js - Date.now = jest.fn(() => { - const tmpDateNow = dateNow; - dateNow += 60000; - return tmpDateNow; - }); - slackMock.sendMessage = jest.fn(); + mockSetupUserWebsocket = jest.fn().mockResolvedValue(true); - mockGetGlobalConfiguration = jest.fn().mockResolvedValue({ - symbols: ['BTCUSDT', 'BNBUSDT'] - }); + mockSyncCandles = jest.fn().mockResolvedValue(true); + mockSetupCandlesWebsocket = jest.fn().mockResolvedValue(true); + mockGetWebsocketCandlesClean = jest.fn().mockResolvedValue(1); - mockGetAccountInfo = jest.fn().mockResolvedValue({ - balances: [ - { - asset: 'BTC' - }, - { - asset: 'BNB' - }, - { - asset: 'ETH' - }, - { - asset: 'USDT' - } - ] - }); + mockSyncATHCandles = jest.fn().mockResolvedValue(true); + mockSetupATHCandlesWebsocket = jest.fn().mockResolvedValue(true); + mockGetWebsocketATHCandlesClean = jest.fn().mockResolvedValue(1); - jest.mock('../cronjob/trailingTradeHelper/configuration', () => ({ - getGlobalConfiguration: mockGetGlobalConfiguration - })); + mockRefreshTickersClean = jest.fn().mockResolvedValue(true); + mockGetWebsocketTickersClean = jest.fn().mockResolvedValue(1); + mockSyncDatabaseOrders = jest.fn().mockResolvedValue(true); - jest.mock('../cronjob/trailingTradeHelper/common', () => ({ - getAccountInfo: mockGetAccountInfo - })); + mockGetGlobalConfiguration = jest.fn().mockResolvedValue({ + symbols: ['BTCUSDT', 'BNBUSDT'] + }); - mockWebsocketCandlesClean = jest.fn().mockResolvedValue(true); - PubSubMock.subscribe = jest.fn().mockImplementation((_key, cb) => { - cb('message', 'data'); - }); + mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + account: 'info' + }); - cacheMock.hget = jest - .fn() - .mockResolvedValue( - JSON.stringify(require('./fixtures/exchange-symbols.json')) - ); - cacheMock.hset = jest.fn().mockResolvedValue(true); + mockCacheExchangeSymbols = jest.fn().mockResolvedValue(true); - binanceMock.client.ws.candles = jest - .fn() - .mockImplementationOnce((_symbols, _interval, cb) => { - cb({ - symbol: 'BTCUSDT' - }); + jest.mock('../cronjob/trailingTradeHelper/configuration', () => ({ + getGlobalConfiguration: mockGetGlobalConfiguration + })); - return mockWebsocketCandlesClean; - }); + jest.mock('../cronjob/trailingTradeHelper/common', () => ({ + getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + lockSymbol: mockLockSymbol, + unlockSymbol: mockUnlockSymbol, + cacheExchangeSymbols: mockCacheExchangeSymbols, + getAPILimit: mockGetAPILimit + })); - const { runBinance } = require('../server-binance'); + jest.mock('../binance/user', () => ({ + setupUserWebsocket: mockSetupUserWebsocket + })); - await runBinance(loggerMock); - jest.advanceTimersByTime(2000); - }); + jest.mock('../binance/orders', () => ({ + syncOpenOrders: mockSyncOpenOrders, + syncDatabaseOrders: mockSyncDatabaseOrders + })); - it('triggers cache.hset', () => { - expect(cacheMock.hset).toHaveBeenCalledWith( - 'trailing-trade-symbols', - 'BTCUSDT-latest-candle', - JSON.stringify({ symbol: 'BTCUSDT' }) - ); - }); + jest.mock('../binance/candles', () => ({ + syncCandles: mockSyncCandles, + setupCandlesWebsocket: mockSetupCandlesWebsocket, + getWebsocketCandlesClean: mockGetWebsocketCandlesClean + })); - it('triggers cache.hset once', () => { - expect(cacheMock.hset).toHaveBeenCalledTimes(1); - }); + jest.mock('../binance/ath-candles', () => ({ + syncATHCandles: mockSyncATHCandles, + setupATHCandlesWebsocket: mockSetupATHCandlesWebsocket, + getWebsocketATHCandlesClean: mockGetWebsocketATHCandlesClean + })); - it('triggers PubSub.subscribe twice', () => { - expect(PubSubMock.subscribe).toHaveBeenCalledTimes(2); - }); + jest.mock('../binance/tickers', () => ({ + refreshTickersClean: mockRefreshTickersClean, + getWebsocketTickersClean: mockGetWebsocketTickersClean, + setupTickersWebsocket: mockSetupTickersWebsocket + })); - it('triggers slack.sendMessage', () => { - expect(slackMock.sendMessage).toHaveBeenCalled(); - }); - }); + mongoMock.deleteAll = jest.fn().mockResolvedValue(true); + + PubSubMock.subscribe = jest.fn().mockResolvedValue(true); + + cacheMock.hget = jest + .fn() + .mockResolvedValue( + JSON.stringify(require('./fixtures/exchange-symbols.json')) + ); - describe('when notifyDebug is not on', () => { - let dateNow = new Date('2021-05-07T00:00:00Z').valueOf(); + cacheMock.hset = jest.fn().mockResolvedValue(true); + }); + + [ + { + label: 'Error -1001', + code: -1001, + sendSlack: false, + featureToggleNotifyDebug: false + }, + { + label: 'Error -1021', + code: -1021, + sendSlack: false, + featureToggleNotifyDebug: true + }, + { + label: 'Error ECONNRESET', + code: 'ECONNRESET', + sendSlack: false, + featureToggleNotifyDebug: false + }, + { + label: 'Error ECONNREFUSED', + code: 'ECONNREFUSED', + sendSlack: false, + featureToggleNotifyDebug: true + }, + { + label: 'Error something else - with notify debug', + code: 'something', + sendSlack: true, + featureToggleNotifyDebug: true + }, + { + label: 'Error something else - without notify debug', + code: 'something', + sendSlack: true, + featureToggleNotifyDebug: false + } + ].forEach(errorInfo => { + describe(`${errorInfo.label}`, () => { beforeEach(async () => { config.get = jest.fn(key => { - switch (key) { - case 'mode': - return 'live'; - case 'featureToggle.notifyDebug': - return false; - default: - return `value-${key}`; + if (key === 'featureToggle.notifyDebug') { + return errorInfo.featureToggleNotifyDebug; } + return null; }); - // Mock Date.now for manipulating moment.js - Date.now = jest.fn(() => { - const tmpDateNow = dateNow; - dateNow += 60000; - return tmpDateNow; - }); - slackMock.sendMessage = jest.fn(); - - mockGetGlobalConfiguration = jest.fn().mockResolvedValue({ - symbols: ['BTCUSDT', 'BNBUSDT'] - }); - - mockGetAccountInfo = jest.fn().mockResolvedValue({ - balances: [ - { - asset: 'BTC' - }, - { - asset: 'BNB' - }, - { - asset: 'ETH' - }, - { - asset: 'USDT' + mockSyncOpenOrders = jest.fn().mockRejectedValueOnce( + new (class CustomError extends Error { + constructor() { + super(); + this.code = errorInfo.code; + this.message = `${errorInfo.featureToggleNotifyDebug} ${errorInfo.label} ${errorInfo.code}`; } - ] - }); - - jest.mock('../cronjob/trailingTradeHelper/configuration', () => ({ - getGlobalConfiguration: mockGetGlobalConfiguration - })); - - jest.mock('../cronjob/trailingTradeHelper/common', () => ({ - getAccountInfo: mockGetAccountInfo - })); - - mockWebsocketCandlesClean = jest.fn().mockResolvedValue(true); - PubSubMock.subscribe = jest.fn().mockImplementation((_key, cb) => { - cb('message', 'data'); - }); - cacheMock.hget = jest - .fn() - .mockResolvedValue( - JSON.stringify(require('./fixtures/exchange-symbols.json')) - ); - cacheMock.hset = jest.fn().mockResolvedValue(true); - - binanceMock.client.ws.candles = jest - .fn() - .mockImplementationOnce((_symbols, _interval, cb) => { - cb({ - symbol: 'BTCUSDT' - }); - - return mockWebsocketCandlesClean; - }); + })() + ); const { runBinance } = require('../server-binance'); - await runBinance(loggerMock); - jest.advanceTimersByTime(2000); }); - it('triggers cache.hset', () => { - expect(cacheMock.hset).toHaveBeenCalledWith( - 'trailing-trade-symbols', - 'BTCUSDT-latest-candle', - JSON.stringify({ symbol: 'BTCUSDT' }) - ); - }); + if (errorInfo.sendSlack) { + it('triggers slack.sendMessage', () => { + expect(mockSlack.sendMessage).toHaveBeenCalled(); + }); + } else { + it('does not trigger slack.sendMessagage', () => { + expect(mockSlack.sendMessage).not.toHaveBeenCalled(); + }); + } + }); + }); - it('triggers cache.hset once', () => { - expect(cacheMock.hset).toHaveBeenCalledTimes(1); - }); + describe(`redlock error`, () => { + beforeEach(async () => { + config.get = jest.fn(_key => null); - it('triggers PubSub.subscribe twice', () => { - expect(PubSubMock.subscribe).toHaveBeenCalledTimes(2); - }); + mockSyncOpenOrders = jest.fn().mockResolvedValue(true); - it('does not trigger slack.sendMessage', () => { - expect(slackMock.sendMessage).not.toHaveBeenCalled(); - }); + mockSetupTickersWebsocket = jest.fn().mockRejectedValueOnce( + new (class CustomError extends Error { + constructor() { + super(); + this.code = 500; + this.message = `redlock:trailing-trade-symbols:XRPBUSD-latest-candle`; + } + })() + ); + + const { runBinance } = require('../server-binance'); + await runBinance(loggerMock); + }); + + it('do not trigger slack.sendMessagage', () => { + expect(mockSlack.sendMessage).not.toHaveBeenCalled(); }); }); }); @@ -539,83 +538,263 @@ describe('server-binance', () => { } }); + mockLockSymbol = jest.fn().mockResolvedValue(true); + mockUnlockSymbol = jest.fn().mockResolvedValue(true); + + mockSetupUserWebsocket = jest.fn().mockResolvedValue(true); + + mockSyncCandles = jest.fn().mockResolvedValue(true); + mockSetupCandlesWebsocket = jest.fn().mockResolvedValue(true); + mockGetWebsocketCandlesClean = jest.fn().mockResolvedValue(44); + + mockSyncATHCandles = jest.fn().mockResolvedValue(true); + mockSetupATHCandlesWebsocket = jest.fn().mockResolvedValue(true); + mockGetWebsocketATHCandlesClean = jest.fn().mockResolvedValue(44); + + mockSetupTickersWebsocket = jest.fn().mockResolvedValue(true); + mockRefreshTickersClean = jest.fn().mockResolvedValue(true); + mockGetWebsocketTickersClean = jest.fn().mockResolvedValue(44); + + mockSyncOpenOrders = jest.fn().mockResolvedValue(true); + mockSyncDatabaseOrders = jest.fn().mockResolvedValue(true); + mockGetGlobalConfiguration = jest.fn().mockResolvedValue({ symbols: ['BTCUSDT', 'ETHUSDT', 'LTCUSDT'] }); - mockGetAccountInfo = jest.fn().mockResolvedValue({ - balances: [ - { - asset: 'BTC' - }, - { - asset: 'BNB' - }, - { - asset: 'ETH' - }, - { - asset: 'USDT' - } - ] + mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + account: 'info' }); + mockCacheExchangeSymbols = jest.fn().mockResolvedValue(true); + jest.mock('../cronjob/trailingTradeHelper/configuration', () => ({ getGlobalConfiguration: mockGetGlobalConfiguration })); jest.mock('../cronjob/trailingTradeHelper/common', () => ({ - getAccountInfo: mockGetAccountInfo + getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + lockSymbol: mockLockSymbol, + unlockSymbol: mockUnlockSymbol, + cacheExchangeSymbols: mockCacheExchangeSymbols + })); + + jest.mock('../binance/user', () => ({ + setupUserWebsocket: mockSetupUserWebsocket + })); + + jest.mock('../binance/orders', () => ({ + syncOpenOrders: mockSyncOpenOrders, + syncDatabaseOrders: mockSyncDatabaseOrders + })); + + jest.mock('../binance/candles', () => ({ + syncCandles: mockSyncCandles, + setupCandlesWebsocket: mockSetupCandlesWebsocket, + getWebsocketCandlesClean: mockGetWebsocketCandlesClean + })); + + jest.mock('../binance/ath-candles', () => ({ + syncATHCandles: mockSyncATHCandles, + setupATHCandlesWebsocket: mockSetupATHCandlesWebsocket, + getWebsocketATHCandlesClean: mockGetWebsocketATHCandlesClean + })); + + jest.mock('../binance/tickers', () => ({ + setupTickersWebsocket: mockSetupTickersWebsocket, + refreshTickersClean: mockRefreshTickersClean, + getWebsocketTickersClean: mockGetWebsocketTickersClean })); - mockWebsocketCandlesClean = jest.fn().mockResolvedValue(true); PubSubMock.subscribe = jest.fn().mockImplementation((_key, cb) => { cb('message', 'data'); }); - cacheMock.hget = jest - .fn() - .mockResolvedValue( - JSON.stringify(require('./fixtures/exchange-symbols.json')) - ); - cacheMock.hset = jest.fn().mockResolvedValue(true); + mongoMock.deleteAll = jest.fn().mockResolvedValue(true); - binanceMock.client.prices = jest.fn().mockResolvedValue({ - BTCUSDT: 30000, - ETHUSDT: 1000, - LTCUSDT: 120, - XRPUSDT: 2 - }); + cacheMock.hset = jest.fn().mockResolvedValue(true); const { runBinance } = require('../server-binance'); await runBinance(loggerMock); + }); + + it('triggers PubSub.subscribe for reset-all-websockets', () => { + expect(PubSubMock.subscribe).toHaveBeenCalledWith( + 'reset-all-websockets', + expect.any(Function) + ); + }); - jest.advanceTimersByTime(1200); + it('triggers PubSub.subscribe for reset-symbol-websockets', () => { + expect(PubSubMock.subscribe).toHaveBeenCalledWith( + 'reset-symbol-websockets', + expect.any(Function) + ); }); it('triggers getGlobalConfiguration', () => { expect(mockGetGlobalConfiguration).toHaveBeenCalled(); }); - it('triggers binance.client.prices', () => { - expect(binanceMock.client.prices).toHaveBeenCalledTimes(2); + it('triggers getAccountInfoFromAPI', () => { + expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); }); - [ - { symbol: 'BTCUSDT', expectedPrice: 30000 }, - { symbol: 'ETHUSDT', expectedPrice: 1000 }, - { symbol: 'LTCUSDT', expectedPrice: 120 } - ].forEach(symbolInfo => { - it(`triggers cache.hset for ${symbolInfo.symbol}`, () => { - expect(cacheMock.hset).toHaveBeenCalledWith( - 'trailing-trade-symbols', - `${symbolInfo.symbol}-latest-candle`, - JSON.stringify({ - eventType: 'kline', - symbol: symbolInfo.symbol, - close: symbolInfo.expectedPrice - }) - ); + it('triggers refreshCandles', () => { + expect(mongoMock.deleteAll).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-candles', + {} + ); + expect(mongoMock.deleteAll).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-ath-candles', + {} + ); + }); + + it('triggers cacheExchangeSymbols', () => { + expect(mockCacheExchangeSymbols).toHaveBeenCalled(); + }); + + it('triggers syncCandles', () => { + expect(mockSyncCandles).toHaveBeenCalledWith(loggerMock, [ + 'BTCUSDT', + 'ETHUSDT', + 'LTCUSDT' + ]); + }); + + it('triggers syncATHCandles', () => { + expect(mockSyncATHCandles).toHaveBeenCalledWith(loggerMock, [ + 'BTCUSDT', + 'ETHUSDT', + 'LTCUSDT' + ]); + }); + + it('triggers syncOpenOrders', () => { + expect(mockSyncOpenOrders).toHaveBeenCalledWith(loggerMock, [ + 'BTCUSDT', + 'ETHUSDT', + 'LTCUSDT' + ]); + }); + + it('triggers syncDatabaseOrders', () => { + expect(mockSyncDatabaseOrders).toHaveBeenCalledWith(loggerMock); + }); + + it('triggers cache.hset', () => { + expect(cacheMock.hset).toHaveBeenCalledWith( + 'trailing-trade-streams', + `count`, + 1 + ); + }); + }); + + describe('when running bot twice', () => { + beforeEach(async () => { + config.get = jest.fn(key => { + switch (key) { + case 'mode': + return 'live'; + default: + return `value-${key}`; + } + }); + + mockLockSymbol = jest.fn().mockResolvedValue(true); + mockUnlockSymbol = jest.fn().mockResolvedValue(true); + + mockSetupUserWebsocket = jest.fn().mockResolvedValue(true); + + mockSyncCandles = jest.fn().mockResolvedValue(true); + mockSetupCandlesWebsocket = jest.fn().mockResolvedValue(true); + mockGetWebsocketCandlesClean = jest.fn().mockResolvedValue(44); + + mockSyncATHCandles = jest.fn().mockResolvedValue(true); + mockSetupATHCandlesWebsocket = jest.fn().mockResolvedValue(true); + mockGetWebsocketATHCandlesClean = jest.fn().mockResolvedValue(44); + + mockSetupTickersWebsocket = jest.fn().mockResolvedValue(true); + mockRefreshTickersClean = jest.fn().mockResolvedValue(true); + mockGetWebsocketTickersClean = jest.fn().mockResolvedValue(44); + + mockSyncOpenOrders = jest.fn().mockResolvedValue(true); + mockSyncDatabaseOrders = jest.fn().mockResolvedValue(true); + + mockGetGlobalConfiguration = jest.fn().mockResolvedValue({ + symbols: ['BTCUSDT', 'ETHUSDT', 'LTCUSDT'] + }); + + mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + account: 'info' + }); + + mockCacheExchangeSymbols = jest.fn().mockResolvedValue(true); + + jest.mock('../cronjob/trailingTradeHelper/configuration', () => ({ + getGlobalConfiguration: mockGetGlobalConfiguration + })); + + jest.mock('../cronjob/trailingTradeHelper/common', () => ({ + getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + lockSymbol: mockLockSymbol, + unlockSymbol: mockUnlockSymbol, + cacheExchangeSymbols: mockCacheExchangeSymbols + })); + + jest.mock('../binance/user', () => ({ + setupUserWebsocket: mockSetupUserWebsocket + })); + + jest.mock('../binance/orders', () => ({ + syncOpenOrders: mockSyncOpenOrders, + syncDatabaseOrders: mockSyncDatabaseOrders + })); + + jest.mock('../binance/candles', () => ({ + syncCandles: mockSyncCandles, + setupCandlesWebsocket: mockSetupCandlesWebsocket, + getWebsocketCandlesClean: mockGetWebsocketCandlesClean + })); + + jest.mock('../binance/ath-candles', () => ({ + syncATHCandles: mockSyncATHCandles, + setupATHCandlesWebsocket: mockSetupATHCandlesWebsocket, + getWebsocketATHCandlesClean: mockGetWebsocketATHCandlesClean + })); + + jest.mock('../binance/tickers', () => ({ + setupTickersWebsocket: mockSetupTickersWebsocket, + refreshTickersClean: mockRefreshTickersClean, + getWebsocketTickersClean: mockGetWebsocketTickersClean + })); + + PubSubMock.subscribe = jest.fn().mockResolvedValue(true); + + mongoMock.deleteAll = jest.fn().mockResolvedValue(true); + + cacheMock.hset = jest.fn().mockResolvedValue(true); + + const { runBinance } = require('../server-binance'); + await runBinance(loggerMock); + await runBinance(loggerMock); + }); + + it('triggers cacheExchangeSymbols', () => { + expect(mockCacheExchangeSymbols).toHaveBeenCalledTimes(2); + }); + + describe('when exchangeSymbolsInterval is passed', () => { + beforeEach(() => { + jest.advanceTimersByTime(61 * 1000); + }); + + it('triggers cacheExchangeSymbols', () => { + expect(mockCacheExchangeSymbols).toHaveBeenCalledTimes(3); }); }); }); diff --git a/app/__tests__/server-cronjob.test.js b/app/__tests__/server-cronjob.test.js index 1d7a4050..bb79cb3a 100644 --- a/app/__tests__/server-cronjob.test.js +++ b/app/__tests__/server-cronjob.test.js @@ -8,7 +8,6 @@ describe('server-cronjob', () => { let mockTaskRunning = false; let mockExecuteAlive; - let mockExecuteTrailingTrade; let mockExecuteTrailingTradeIndicator; describe('cronjob running fine', () => { @@ -19,12 +18,10 @@ describe('server-cronjob', () => { cache.hset = jest.fn().mockResolvedValue(true); mockExecuteAlive = jest.fn().mockResolvedValue(true); - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); mockExecuteTrailingTradeIndicator = jest.fn().mockResolvedValue(true); jest.mock('../cronjob', () => ({ executeAlive: mockExecuteAlive, - executeTrailingTrade: mockExecuteTrailingTrade, executeTrailingTradeIndicator: mockExecuteTrailingTradeIndicator })); @@ -104,67 +101,6 @@ describe('server-cronjob', () => { }); }); - describe('trailingTrade', () => { - beforeEach(async () => { - config.get = jest.fn(key => { - switch (key) { - case 'jobs.trailingTrade.enabled': - return true; - case 'jobs.trailingTrade.cronTime': - return '* * * * * *'; - case 'tz': - return 'Australia/Melbourne'; - default: - return `value-${key}`; - } - }); - }); - - describe('when task is already running', () => { - beforeEach(() => { - mockTaskRunning = true; - const { runCronjob } = require('../server-cronjob'); - runCronjob(logger); - }); - - it('initialise CronJob', () => { - expect(mockCronJob).toHaveBeenCalledWith( - '* * * * * *', - expect.any(Function), - null, - false, - 'Australia/Melbourne' - ); - }); - - it('does not trigger executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).not.toHaveBeenCalled(); - }); - }); - - describe('when task is not running', () => { - beforeEach(() => { - mockTaskRunning = false; - const { runCronjob } = require('../server-cronjob'); - runCronjob(logger); - }); - - it('initialise CronJob', () => { - expect(mockCronJob).toHaveBeenCalledWith( - '* * * * * *', - expect.any(Function), - null, - false, - 'Australia/Melbourne' - ); - }); - - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalled(); - }); - }); - }); - describe('trailingTradeIndicator', () => { beforeEach(async () => { config.get = jest.fn(key => { @@ -247,8 +183,8 @@ describe('server-cronjob', () => { expect(mockExecuteAlive).not.toHaveBeenCalled(); }); - it('does not trigger executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).not.toHaveBeenCalled(); + it('does not trigger executeTrailingTradeIndicator', () => { + expect(mockExecuteTrailingTradeIndicator).not.toHaveBeenCalled(); }); }); }); @@ -262,14 +198,12 @@ describe('server-cronjob', () => { cache.hset = jest.fn().mockResolvedValue(true); mockExecuteAlive = jest.fn().mockResolvedValue(true); - mockExecuteTrailingTrade = jest.fn().mockImplementation(() => { + mockExecuteTrailingTradeIndicator = jest.fn().mockImplementation(() => { setTimeout(() => Promise.resolve(true), 30000); }); - mockExecuteTrailingTradeIndicator = jest.fn().mockResolvedValue(true); jest.mock('../cronjob', () => ({ executeAlive: mockExecuteAlive, - executeTrailingTrade: mockExecuteTrailingTrade, executeTrailingTradeIndicator: mockExecuteTrailingTradeIndicator })); @@ -289,9 +223,9 @@ describe('server-cronjob', () => { config.get = jest.fn(key => { switch (key) { - case 'jobs.trailingTrade.enabled': + case 'jobs.trailingTradeIndicator.enabled': return true; - case 'jobs.trailingTrade.cronTime': + case 'jobs.trailingTradeIndicator.cronTime': return '* * * * * *'; case 'tz': return 'Australia/Melbourne'; @@ -320,8 +254,8 @@ describe('server-cronjob', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalled(); + it('triggers executeTrailingTradeIndicator', () => { + expect(mockExecuteTrailingTradeIndicator).toHaveBeenCalled(); }); }); }); diff --git a/app/binance/__tests__/ath-candles.test.js b/app/binance/__tests__/ath-candles.test.js new file mode 100644 index 00000000..e9b5ad7d --- /dev/null +++ b/app/binance/__tests__/ath-candles.test.js @@ -0,0 +1,365 @@ +/* eslint-disable global-require */ +describe('ath-candles.js', () => { + let athCandles; + let binanceMock; + let loggerMock; + let mongoMock; + + let mockGetConfiguration; + let mockSaveCandle; + + let mockWebsocketATHCandlesClean; + + beforeEach(() => { + jest.clearAllMocks().resetModules(); + }); + + describe('when ATH enabled', () => { + describe('setupATHCandlesWebsocket', () => { + beforeEach(async () => { + const { binance, logger } = require('../../helpers'); + binanceMock = binance; + loggerMock = logger; + + mockGetConfiguration = jest + .fn() + .mockImplementation((_logger, _symbol) => ({ + buy: { + athRestriction: { + enabled: true, + candles: { interval: '1d' } + } + } + })); + + mockSaveCandle = jest.fn().mockResolvedValue(true); + + mockWebsocketATHCandlesClean = jest.fn(); + + binanceMock.client.ws.candles = jest.fn( + (symbols, candleInterval, fn) => { + fn({ + eventType: 'kline', + eventTime: 1525285576276, + symbol: 'ETHBTC', + startTime: 1525285560000, + open: '0.04898000', + high: '0.04902700', + low: '0.04898000', + close: '0.04901900', + closeTime: 1525285619999, + volume: '37.89600000', + trades: 30, + interval: '30m', + isFinal: false, + quoteVolume: '1.85728874', + buyVolume: '21.79900000', + quoteBuyVolume: '1.06838790' + }); + + return mockWebsocketATHCandlesClean; + } + ); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + saveCandle: mockSaveCandle + })); + + jest.mock('../../cronjob/trailingTradeHelper/configuration', () => ({ + getConfiguration: mockGetConfiguration + })); + + athCandles = require('../ath-candles'); + + await athCandles.setupATHCandlesWebsocket(loggerMock, [ + 'ETHBTC', + 'BNBUSDT' + ]); + }); + + it('triggers getConfiguration twice', () => { + expect(mockGetConfiguration).toHaveBeenCalledTimes(2); + }); + + it('triggers getConfiguration for ETHBTC', () => { + expect(mockGetConfiguration).toHaveBeenCalledWith(loggerMock, 'ETHBTC'); + }); + + it('triggers getConfiguration for BNBUSDT', () => { + expect(mockGetConfiguration).toHaveBeenCalledWith( + loggerMock, + 'BNBUSDT' + ); + }); + + it('triggers saveCandle', () => { + expect(mockSaveCandle).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-ath-candles', + { + close: 0.049019, + high: 0.049027, + interval: '30m', + key: 'ETHBTC', + low: 0.04898, + open: 0.04898, + time: 1525285560000, + volume: 37.896 + } + ); + }); + + it('checks websocketATHCandlesClean', () => { + expect(athCandles.getWebsocketATHCandlesClean()).toStrictEqual({ + '1d': expect.any(Function) + }); + }); + + it('does not trigger websocketATHCandlesClean', () => { + expect(mockWebsocketATHCandlesClean).not.toHaveBeenCalled(); + }); + + describe('when called again', () => { + beforeEach(async () => { + mockGetConfiguration.mockClear(); + await athCandles.setupATHCandlesWebsocket(loggerMock, ['BTCUSDT']); + }); + + it('triggers getConfiguration twice', () => { + expect(mockGetConfiguration).toHaveBeenCalledTimes(1); + }); + + it('triggers getConfiguration for BTCUSDT', () => { + expect(mockGetConfiguration).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); + + it('triggers websocketATHCandlesClean', () => { + expect(mockWebsocketATHCandlesClean).toHaveBeenCalledTimes(1); + }); + }); + }); + describe('syncATHCandles', () => { + beforeEach(async () => { + const { binance, logger, mongo } = require('../../helpers'); + binanceMock = binance; + loggerMock = logger; + mongoMock = mongo; + + mockGetConfiguration = jest + .fn() + .mockImplementation((_logger, _symbol) => ({ + buy: { + athRestriction: { + enabled: true, + candles: { interval: '1d', limit: 1 } + } + } + })); + + mockSaveCandle = jest.fn().mockResolvedValue(true); + + mongoMock.deleteAll = jest.fn().mockResolvedValue(true); + + binanceMock.client.candles = jest.fn().mockImplementation(options => { + const { symbol } = options; + + if (symbol === 'ETHBTC') { + return [ + { + openTime: 1508328900000, + open: '0.05655000', + high: '0.05656500', + low: '0.05613200', + close: '0.05632400', + volume: '68.88800000', + closeTime: 1508329199999, + quoteAssetVolume: '2.29500857', + trades: 85, + baseAssetVolume: '40.61900000' + } + ]; + } + + return []; + }); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + saveCandle: mockSaveCandle + })); + + jest.mock('../../cronjob/trailingTradeHelper/configuration', () => ({ + getConfiguration: mockGetConfiguration + })); + + athCandles = require('../ath-candles'); + + await athCandles.syncATHCandles(loggerMock, ['ETHBTC']); + }); + + it('triggers mongo.deleteAll', () => { + expect(mongoMock.deleteAll).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-ath-candles', + { + key: 'ETHBTC' + } + ); + }); + + it('triggers getConfiguration for ETHBTC', () => { + expect(mockGetConfiguration).toHaveBeenCalledWith(loggerMock, 'ETHBTC'); + }); + + it('triggers binance.client.candles', () => { + expect(binanceMock.client.candles).toHaveBeenCalledWith({ + interval: '1d', + limit: 1, + symbol: 'ETHBTC' + }); + }); + + it('triggers saveCandle one time', () => { + expect(mockSaveCandle).toHaveBeenCalledTimes(1); + }); + + it('triggers saveCandle with expected parameters', () => { + expect(mockSaveCandle).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-ath-candles', + { + close: 0.056324, + high: 0.056565, + interval: '1d', + key: 'ETHBTC', + low: 0.056132, + open: 0.05655, + time: 1508328900000, + volume: 68.888 + } + ); + }); + }); + }); + + describe('when ATH disabled', () => { + describe('setupATHCandlesWebsocket', () => { + beforeEach(async () => { + const { binance, logger } = require('../../helpers'); + binanceMock = binance; + loggerMock = logger; + + mockGetConfiguration = jest + .fn() + .mockImplementation((_logger, _symbol) => ({ + buy: { + athRestriction: { + enabled: false, + candles: { interval: '1d' } + } + } + })); + + mockSaveCandle = jest.fn().mockResolvedValue(true); + + mockWebsocketATHCandlesClean = jest.fn(); + + binanceMock.client.ws.candles = jest.fn().mockResolvedValue(true); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + saveCandle: mockSaveCandle + })); + + jest.mock('../../cronjob/trailingTradeHelper/configuration', () => ({ + getConfiguration: mockGetConfiguration + })); + + athCandles = require('../ath-candles'); + + await athCandles.setupATHCandlesWebsocket(loggerMock, ['ETHBTC']); + }); + + it('triggers getConfiguration', () => { + expect(mockGetConfiguration).toHaveBeenCalledWith(loggerMock, 'ETHBTC'); + }); + + it('does not trigger saveCandle', () => { + expect(mockSaveCandle).not.toHaveBeenCalled(); + }); + + it('does not trigger binance.client.ws.candles', () => { + expect(binanceMock.client.ws.candles).not.toHaveBeenCalled(); + }); + + it('checks websocketATHCandlesClean', () => { + expect(athCandles.getWebsocketATHCandlesClean()).toStrictEqual({}); + }); + + it('does not trigger websocketATHCandlesClean', () => { + expect(mockWebsocketATHCandlesClean).not.toHaveBeenCalled(); + }); + }); + + describe('syncATHCandles', () => { + beforeEach(async () => { + const { binance, logger, mongo } = require('../../helpers'); + binanceMock = binance; + loggerMock = logger; + mongoMock = mongo; + + mockGetConfiguration = jest + .fn() + .mockImplementation((_logger, _symbol) => ({ + buy: { + athRestriction: { + enabled: false, + candles: { interval: '1d', limit: 1 } + } + } + })); + + mockSaveCandle = jest.fn().mockResolvedValue(true); + + mongoMock.deleteAll = jest.fn().mockResolvedValue(true); + + binanceMock.client.candles = jest.fn().mockResolvedValue(true); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + saveCandle: mockSaveCandle + })); + + jest.mock('../../cronjob/trailingTradeHelper/configuration', () => ({ + getConfiguration: mockGetConfiguration + })); + + athCandles = require('../ath-candles'); + + await athCandles.syncATHCandles(loggerMock, ['ETHBTC']); + }); + + it('triggers mongo.deleteAll', () => { + expect(mongoMock.deleteAll).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-ath-candles', + { + key: 'ETHBTC' + } + ); + }); + + it('triggers getConfiguration', () => { + expect(mockGetConfiguration).toHaveBeenCalledWith(loggerMock, 'ETHBTC'); + }); + + it('does not trigger binance.client.candles', () => { + expect(binanceMock.client.candles).not.toHaveBeenCalled(); + }); + + it('does not trigger saveCandle', () => { + expect(mockSaveCandle).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/app/binance/__tests__/candles.test.js b/app/binance/__tests__/candles.test.js new file mode 100644 index 00000000..6bd88c23 --- /dev/null +++ b/app/binance/__tests__/candles.test.js @@ -0,0 +1,227 @@ +/* eslint-disable global-require */ +describe('candles.js', () => { + let candles; + let binanceMock; + let loggerMock; + let mongoMock; + + let mockGetConfiguration; + let mockSaveCandle; + + let mockWebsocketCandlesClean; + + beforeEach(() => { + jest.clearAllMocks().resetModules(); + }); + + describe('setupCandlesWebsocket', () => { + beforeEach(async () => { + const { binance, logger } = require('../../helpers'); + binanceMock = binance; + loggerMock = logger; + + mockGetConfiguration = jest + .fn() + .mockImplementation((_logger, _symbol) => ({ + candles: { interval: '30m' } + })); + + mockSaveCandle = jest.fn().mockResolvedValue(true); + + mockWebsocketCandlesClean = jest.fn(); + + binanceMock.client.ws.candles = jest.fn((symbols, candleInterval, fn) => { + fn({ + eventType: 'kline', + eventTime: 1525285576276, + symbol: 'ETHBTC', + startTime: 1525285560000, + open: '0.04898000', + high: '0.04902700', + low: '0.04898000', + close: '0.04901900', + closeTime: 1525285619999, + volume: '37.89600000', + trades: 30, + interval: '30m', + isFinal: false, + quoteVolume: '1.85728874', + buyVolume: '21.79900000', + quoteBuyVolume: '1.06838790' + }); + + return mockWebsocketCandlesClean; + }); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + saveCandle: mockSaveCandle + })); + + jest.mock('../../cronjob/trailingTradeHelper/configuration', () => ({ + getConfiguration: mockGetConfiguration + })); + + candles = require('../candles'); + + await candles.setupCandlesWebsocket(loggerMock, ['ETHBTC', 'BNBUSDT']); + }); + + it('triggers getConfiguration twice', () => { + expect(mockGetConfiguration).toHaveBeenCalledTimes(2); + }); + + it('triggers getConfiguration for ETHBTC', () => { + expect(mockGetConfiguration).toHaveBeenCalledWith(loggerMock, 'ETHBTC'); + }); + + it('triggers getConfiguration for BNBUSDT', () => { + expect(mockGetConfiguration).toHaveBeenCalledWith(loggerMock, 'BNBUSDT'); + }); + + it('triggers saveCandle', () => { + expect(mockSaveCandle).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-candles', + { + close: 0.049019, + high: 0.049027, + interval: '30m', + key: 'ETHBTC', + low: 0.04898, + open: 0.04898, + time: 1525285560000, + volume: 37.896 + } + ); + }); + + it('checks websocketCandlesClean', () => { + expect(candles.getWebsocketCandlesClean()).toStrictEqual({ + '30m': expect.any(Function) + }); + }); + + it('does not trigger websocketCandlesClean', () => { + expect(mockWebsocketCandlesClean).not.toHaveBeenCalled(); + }); + + describe('when called again', () => { + beforeEach(async () => { + mockGetConfiguration.mockClear(); + await candles.setupCandlesWebsocket(loggerMock, ['BTCUSDT']); + }); + + it('triggers getConfiguration twice', () => { + expect(mockGetConfiguration).toHaveBeenCalledTimes(1); + }); + + it('triggers getConfiguration for BTCUSDT', () => { + expect(mockGetConfiguration).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); + + it('triggers websocketCandlesClean', () => { + expect(mockWebsocketCandlesClean).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('syncCandles', () => { + beforeEach(async () => { + const { binance, logger, mongo } = require('../../helpers'); + binanceMock = binance; + loggerMock = logger; + mongoMock = mongo; + + mockGetConfiguration = jest + .fn() + .mockImplementation((_logger, _symbol) => ({ + candles: { interval: '30m', limit: 1 } + })); + + mockSaveCandle = jest.fn().mockResolvedValue(true); + + mongoMock.deleteAll = jest.fn().mockResolvedValue(true); + + binanceMock.client.candles = jest.fn().mockImplementation(options => { + const { symbol } = options; + + if (symbol === 'ETHBTC') { + return [ + { + openTime: 1508328900000, + open: '0.05655000', + high: '0.05656500', + low: '0.05613200', + close: '0.05632400', + volume: '68.88800000', + closeTime: 1508329199999, + quoteAssetVolume: '2.29500857', + trades: 85, + baseAssetVolume: '40.61900000' + } + ]; + } + + return []; + }); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + saveCandle: mockSaveCandle + })); + + jest.mock('../../cronjob/trailingTradeHelper/configuration', () => ({ + getConfiguration: mockGetConfiguration + })); + + candles = require('../candles'); + + await candles.syncCandles(loggerMock, ['ETHBTC']); + }); + + it('triggers mongo.deleteAll', () => { + expect(mongoMock.deleteAll).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-candles', + { + key: 'ETHBTC' + } + ); + }); + + it('triggers getConfiguration for ETHBTC', () => { + expect(mockGetConfiguration).toHaveBeenCalledWith(loggerMock, 'ETHBTC'); + }); + + it('triggers binance.client.candles', () => { + expect(binanceMock.client.candles).toHaveBeenCalledWith({ + interval: '30m', + limit: 1, + symbol: 'ETHBTC' + }); + }); + + it('triggers saveCandle one time', () => { + expect(mockSaveCandle).toHaveBeenCalledTimes(1); + }); + + it('triggers saveCandle with expected parameters', () => { + expect(mockSaveCandle).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-candles', + { + close: 0.056324, + high: 0.056565, + interval: '30m', + key: 'ETHBTC', + low: 0.056132, + open: 0.05655, + time: 1508328900000, + volume: 68.888 + } + ); + }); + }); +}); diff --git a/app/binance/__tests__/orders.test.js b/app/binance/__tests__/orders.test.js new file mode 100644 index 00000000..31ab4800 --- /dev/null +++ b/app/binance/__tests__/orders.test.js @@ -0,0 +1,241 @@ +/* eslint-disable global-require */ + +describe('orders.js', () => { + let binanceMock; + let loggerMock; + let mongoMock; + let cacheMock; + let spyOnClearInterval; + + let mockUpdateGridTradeLastOrder; + let mockGetOpenOrdersFromAPI; + + beforeEach(async () => { + jest.clearAllMocks().resetModules(); + + const { binance, logger, cache, mongo } = require('../../helpers'); + binanceMock = binance; + loggerMock = logger; + cacheMock = cache; + mongoMock = mongo; + }); + + describe('syncOpenOrders', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + describe('when open orders are retrieved', () => { + beforeEach(async () => { + cacheMock.hset = jest.fn().mockResolvedValue(true); + + mockGetOpenOrdersFromAPI = jest.fn().mockResolvedValue([ + { + symbol: 'BTCUSDT', + orderId: 46838, + price: '1799.58000000', + type: 'LIMIT', + side: 'BUY' + } + ]); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + getOpenOrdersFromAPI: mockGetOpenOrdersFromAPI + })); + + const { syncOpenOrders } = require('../orders'); + + await syncOpenOrders(loggerMock, ['BTCUSDT', 'BNBUSDT']); + + jest.advanceTimersByTime(30 * 1340); + }); + + it('triggers getOpenOrdersFromAPI', () => { + expect(mockGetOpenOrdersFromAPI).toHaveBeenCalled(); + }); + + it('triggers cache.hset', () => { + expect(cacheMock.hset).toHaveBeenCalledWith( + 'trailing-trade-open-orders', + 'BTCUSDT', + JSON.stringify([ + { + symbol: 'BTCUSDT', + orderId: 46838, + price: '1799.58000000', + type: 'LIMIT', + side: 'BUY' + } + ]) + ); + }); + }); + describe('when open orders are empty', () => { + beforeEach(async () => { + cacheMock.hset = jest.fn().mockResolvedValue(true); + + mockGetOpenOrdersFromAPI = jest.fn().mockResolvedValue([]); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + getOpenOrdersFromAPI: mockGetOpenOrdersFromAPI + })); + + const { syncOpenOrders } = require('../orders'); + + await syncOpenOrders(loggerMock, ['BTCUSDT', 'BNBUSDT']); + + jest.advanceTimersByTime(30 * 1340); + }); + + it('triggers getOpenOrdersFromAPI', () => { + expect(mockGetOpenOrdersFromAPI).toHaveBeenCalled(); + }); + + it('triggers cache.hset', () => { + expect(cacheMock.hset).toHaveBeenCalledWith( + 'trailing-trade-open-orders', + 'BTCUSDT', + JSON.stringify([]) + ); + }); + }); + + describe('when openOrdersInterval is not empty', () => { + beforeEach(async () => { + cacheMock.hset = jest.fn().mockResolvedValue(true); + + loggerMock.error = jest.fn().mockResolvedValue(true); + + mockGetOpenOrdersFromAPI = jest.fn().mockRejectedValue({ + error: 'error thrown' + }); + + const spyOnSetInterval = jest.spyOn(global, 'setInterval'); + spyOnClearInterval = jest.spyOn(global, 'clearInterval'); + spyOnSetInterval.mockReturnValueOnce(33); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + getOpenOrdersFromAPI: mockGetOpenOrdersFromAPI + })); + + const { syncOpenOrders } = require('../orders'); + + await syncOpenOrders(loggerMock, ['BTCUSDT', 'BNBUSDT']); + await syncOpenOrders(loggerMock, ['BTCUSDT', 'BNBUSDT']); + }); + + it('triggers clearInterval', () => { + expect(spyOnClearInterval).toHaveBeenCalledWith(33); + }); + }); + }); + + describe('syncDatabaseOrders', () => { + describe('when database orders found', () => { + beforeEach(async () => { + mongoMock.findAll = jest.fn().mockResolvedValue([ + { + order: { + symbol: 'BTCUSDT', + cummulativeQuoteQty: '0.00000000', + executedQty: '0.00000000', + isWorking: false, + orderId: 7479643460, + origQty: '0.00920000', + price: '3248.37000000', + side: 'BUY', + status: 'NEW', + stopPrice: '3245.19000000', + type: 'STOP_LOSS_LIMIT', + updateTime: 1642713283562 + } + } + ]); + + binanceMock.client.getOrder = jest.fn().mockResolvedValue({ + symbol: 'BTCUSDT', + cummulativeQuoteQty: '0.00000000', + executedQty: '0.00000000', + isWorking: false, + orderId: 7479643460, + origQty: '0.00920000', + price: '3248.37000000', + side: 'BUY', + status: 'CANCELED', + stopPrice: '3245.19000000', + type: 'STOP_LOSS_LIMIT', + updateTime: 1642713283562 + }); + + mockUpdateGridTradeLastOrder = jest.fn().mockResolvedValue(true); + + jest.mock('../../cronjob/trailingTradeHelper/order', () => ({ + updateGridTradeLastOrder: mockUpdateGridTradeLastOrder + })); + + const { syncDatabaseOrders } = require('../orders'); + + await syncDatabaseOrders(loggerMock); + }); + + it('triggers mongo.findAll', () => { + expect(mongoMock.findAll).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-grid-trade-orders', + {} + ); + }); + + it('triggers updateGridTradeLastOrder', () => { + expect(mockUpdateGridTradeLastOrder).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT', + 'buy', + { + symbol: 'BTCUSDT', + cummulativeQuoteQty: '0.00000000', + executedQty: '0.00000000', + isWorking: false, + orderId: 7479643460, + origQty: '0.00920000', + price: '3248.37000000', + side: 'BUY', + status: 'CANCELED', + stopPrice: '3245.19000000', + type: 'STOP_LOSS_LIMIT', + updateTime: 1642713283562 + } + ); + }); + }); + describe('when database orders not found', () => { + beforeEach(async () => { + mongoMock.findAll = jest.fn().mockResolvedValue([]); + + binanceMock.client.getOrder = jest.fn().mockResolvedValue(true); + + mockUpdateGridTradeLastOrder = jest.fn().mockResolvedValue(true); + + jest.mock('../../cronjob/trailingTradeHelper/order', () => ({ + updateGridTradeLastOrder: mockUpdateGridTradeLastOrder + })); + + const { syncDatabaseOrders } = require('../orders'); + + await syncDatabaseOrders(loggerMock); + }); + + it('triggers mongo.findAll', () => { + expect(mongoMock.findAll).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-grid-trade-orders', + {} + ); + }); + + it('does not trigger updateGridTradeLastOrder', () => { + expect(mockUpdateGridTradeLastOrder).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/app/binance/__tests__/tickers.test.js b/app/binance/__tests__/tickers.test.js new file mode 100644 index 00000000..47895a31 --- /dev/null +++ b/app/binance/__tests__/tickers.test.js @@ -0,0 +1,178 @@ +/* eslint-disable global-require */ +describe('tickers.js', () => { + let tickers; + let binanceMock; + let loggerMock; + let cacheMock; + + let mockGetAccountInfo; + let mockGetCachedExchangeSymbols; + let mockExecuteTrailingTrade; + + let mockWebsocketTickersClean; + + beforeEach(() => { + jest.clearAllMocks().resetModules(); + }); + + describe('setupTickersWebsocket', () => { + beforeEach(async () => { + const { binance, logger, cache } = require('../../helpers'); + binanceMock = binance; + loggerMock = logger; + cacheMock = cache; + + mockGetAccountInfo = jest.fn().mockResolvedValue({ + balances: [ + { asset: 'BTC', free: '0.00100000', locked: '0.99900000' }, + { asset: 'BNB', free: '0.00100000', locked: '0.99900000' } + ] + }); + mockGetCachedExchangeSymbols = jest.fn().mockResolvedValue({ + BNBBUSD: { symbol: 'BNBBUSD', quoteAsset: 'BUSD', minNotional: 10 }, + BTCBUSD: { symbol: 'BTCBUSD', quoteAsset: 'BUSD', minNotional: 10 }, + BNBBTC: { symbol: 'BNBBTC', quoteAsset: 'BTC', minNotional: 0.0001 }, + ETHBTC: { symbol: 'ETHBTC', quoteAsset: 'BTC', minNotional: 0.0001 } + }); + + cacheMock.hset = jest.fn().mockResolvedValue(true); + + mockWebsocketTickersClean = jest.fn(); + + binanceMock.client.ws.miniTicker = jest.fn((_symbol, fn) => { + fn({ + eventType: '24hrMiniTicker', + eventTime: 1658062447261, + symbol: 'BTCUSDT', + curDayClose: '21391.62000000', + open: '20701.45000000', + high: '21689.60000000', + low: '20257.25000000', + volume: '7606.25866900', + volumeQuote: '161554176.59059007' + }); + + return mockWebsocketTickersClean; + }); + + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + getAccountInfo: mockGetAccountInfo, + getCachedExchangeSymbols: mockGetCachedExchangeSymbols + })); + + jest.mock('../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); + + tickers = require('../tickers'); + + await tickers.setupTickersWebsocket(loggerMock, ['BTCUSDT', 'BNBUSDT']); + }); + + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalledWith(loggerMock); + }); + + it('triggers getCachedExchangeSymbols', () => { + expect(mockGetCachedExchangeSymbols).toHaveBeenCalledWith(loggerMock); + }); + + it('triggers cache.hset', () => { + expect(cacheMock.hset).toHaveBeenCalledWith( + 'trailing-trade-symbols', + 'BTCUSDT-latest-candle', + JSON.stringify({ + eventType: '24hrMiniTicker', + eventTime: 1658062447261, + symbol: 'BTCUSDT', + close: '21391.62000000' + }) + ); + }); + + it('triggers executeTrailingTrade twice', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledTimes(2); + }); + + it('triggers executeTrailingTrade for BTCUSDT', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); + + it('triggers executeTrailingTrade for BNBUSDT', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + loggerMock, + 'BNBUSDT' + ); + }); + + it('checks websocketTickersClean', () => { + expect(tickers.getWebsocketTickersClean()).toStrictEqual({ + BNBBTC: expect.any(Function), + BNBUSDT: expect.any(Function), + BTCUSDT: expect.any(Function) + }); + }); + + it('does not trigger websocketTickersClean', () => { + expect(mockWebsocketTickersClean).not.toHaveBeenCalled(); + }); + + describe('when called again', () => { + beforeEach(async () => { + // Reset mock counter + mockExecuteTrailingTrade.mockClear(); + await tickers.setupTickersWebsocket(loggerMock, ['BTCUSDT']); + }); + + it('triggers executeTrailingTrade twice', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledTimes(1); + }); + + it('triggers executeTrailingTrade for BTCUSDT', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); + + it('triggers websocketTickersClean', () => { + expect(mockWebsocketTickersClean).toHaveBeenCalledTimes(2); + }); + }); + + describe('if called refreshTickersClean', () => { + beforeEach(async () => { + // Reset mock counter + mockWebsocketTickersClean.mockClear(); + + await tickers.refreshTickersClean(loggerMock); + }); + + it('triggers websocketTickersClean 3 times', () => { + expect(mockWebsocketTickersClean).toHaveBeenCalledTimes(3); + }); + + it('checks websocketTickersClean', () => { + expect(tickers.getWebsocketTickersClean()).toStrictEqual({}); + }); + + describe('if called refreshTickersClean one more time', () => { + beforeEach(async () => { + // Reset mock counter + mockWebsocketTickersClean.mockClear(); + + await tickers.refreshTickersClean(loggerMock); + }); + + it('does not trigger websocketTickersClean times', () => { + expect(mockWebsocketTickersClean).not.toHaveBeenCalled(); + }); + }); + }); + }); +}); diff --git a/app/binance/__tests__/user.test.js b/app/binance/__tests__/user.test.js new file mode 100644 index 00000000..64e0579a --- /dev/null +++ b/app/binance/__tests__/user.test.js @@ -0,0 +1,572 @@ +/* eslint-disable global-require */ + +describe('user.js', () => { + let binanceMock; + let loggerMock; + + let mockGetAccountInfoFromAPI; + let mockUpdateAccountInfo; + let mockGridTradeLastOrder; + let mockUpdateGridTradeLastOrder; + let mockGetManualOrder; + let mockSaveManualOrder; + + let mockUserClean; + + describe('setupUserWebsocket', () => { + beforeEach(async () => { + jest.clearAllMocks().resetModules(); + + const { binance, logger } = require('../../helpers'); + binanceMock = binance; + loggerMock = logger; + }); + + describe('when balanceUpdate event received', () => { + beforeEach(async () => { + mockUserClean = jest.fn().mockResolvedValue(true); + + mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + account: 'info' + }); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + getAccountInfoFromAPI: mockGetAccountInfoFromAPI + })); + + binanceMock.client.ws.user = jest.fn().mockImplementationOnce(cb => { + cb({ eventType: 'balanceUpdate' }); + + return mockUserClean; + }); + + const { setupUserWebsocket } = require('../user'); + + await setupUserWebsocket(loggerMock); + }); + + it('triggers user', () => { + expect(binanceMock.client.ws.user).toHaveBeenCalled(); + }); + + it('triggers getAccountInfoFromAPI', () => { + expect(mockGetAccountInfoFromAPI).toHaveBeenCalledWith(loggerMock); + }); + + it('does not trigger userClean', () => { + expect(mockUserClean).not.toHaveBeenCalled(); + }); + }); + + describe('when account event received', () => { + beforeEach(async () => { + mockUserClean = jest.fn().mockResolvedValue(true); + + mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + account: 'info' + }); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + getAccountInfoFromAPI: mockGetAccountInfoFromAPI + })); + + binanceMock.client.ws.user = jest.fn().mockImplementationOnce(cb => { + cb({ eventType: 'account' }); + + return mockUserClean; + }); + + const { setupUserWebsocket } = require('../user'); + + await setupUserWebsocket(loggerMock); + }); + + it('triggers user', () => { + expect(binanceMock.client.ws.user).toHaveBeenCalled(); + }); + + it('triggers getAccountInfoFromAPI', () => { + expect(mockGetAccountInfoFromAPI).toHaveBeenCalledWith(loggerMock); + }); + + it('does not trigger userClean', () => { + expect(mockUserClean).not.toHaveBeenCalled(); + }); + }); + + describe('when outboundAccountPosition event received', () => { + beforeEach(async () => { + mockUserClean = jest.fn().mockResolvedValue(true); + + mockUpdateAccountInfo = jest.fn().mockResolvedValue({ + account: 'updated' + }); + + jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ + updateAccountInfo: mockUpdateAccountInfo + })); + + binanceMock.client.ws.user = jest.fn().mockImplementationOnce(cb => { + cb({ + eventType: 'outboundAccountPosition', + balances: [ + { asset: 'ADA', free: '0.00000000', locked: '13.82000000' } + ], + lastAccountUpdate: 1625585531721 + }); + + return mockUserClean; + }); + + const { setupUserWebsocket } = require('../user'); + + await setupUserWebsocket(loggerMock); + }); + + it('triggers updateAccountInfo', () => { + expect(mockUpdateAccountInfo).toHaveBeenCalledWith( + loggerMock, + [{ asset: 'ADA', free: '0.00000000', locked: '13.82000000' }], + 1625585531721 + ); + }); + + it('does not trigger userClean', () => { + expect(mockUserClean).not.toHaveBeenCalled(); + }); + }); + + describe('when executionReport event received', () => { + describe('when last order not found', () => { + beforeEach(async () => { + mockUserClean = jest.fn().mockResolvedValue(true); + + mockGridTradeLastOrder = jest.fn().mockResolvedValue(null); + mockUpdateGridTradeLastOrder = jest.fn().mockResolvedValue(null); + mockGetManualOrder = jest.fn().mockResolvedValue(null); + mockSaveManualOrder = jest.fn().mockResolvedValue(null); + + jest.mock('../../cronjob/trailingTradeHelper/order', () => ({ + getGridTradeLastOrder: mockGridTradeLastOrder, + updateGridTradeLastOrder: mockUpdateGridTradeLastOrder, + getManualOrder: mockGetManualOrder, + saveManualOrder: mockSaveManualOrder + })); + + binanceMock.client.ws.user = jest.fn().mockImplementationOnce(cb => { + /** + * Sample execution report + * { + * "eventType": "executionReport", + * "eventTime": 1642713283562, + * "symbol": "ETHUSDT", + * "newClientOrderId": "R4gzUYn9pQbOA3vAkgKTSw", + * "originalClientOrderId": "", + * "side": "BUY", + * "orderType": "STOP_LOSS_LIMIT", + * "timeInForce": "GTC", + * "quantity": "0.00920000", + * "price": "3248.37000000", + * "executionType": "NEW", + * "stopPrice": "3245.19000000", + * "icebergQuantity": "0.00000000", + * "orderStatus": "NEW", + * "orderRejectReason": "NONE", + * "orderId": 7479643460, + * "orderTime": 1642713283561, + * "lastTradeQuantity": "0.00000000", + * "totalTradeQuantity": "0.00000000", + * "priceLastTrade": "0.00000000", + * "commission": "0", + * "commissionAsset": null, + * "tradeId": -1, + * "isOrderWorking": false, + * "isBuyerMaker": false, + * "creationTime": 1642713283561, + * "totalQuoteTradeQuantity": "0.00000000", + * "orderListId": -1, + * "quoteOrderQuantity": "0.00000000", + * "lastQuoteTransacted": "0.00000000" + * } + */ + cb({ + eventType: 'executionReport', + eventTime: 1642713283562, + symbol: 'ETHUSDT', + side: 'BUY', + orderStatus: 'NEW', + orderType: 'STOP_LOSS_LIMIT', + stopPrice: '3245.19000000', + price: '3248.37000000', + orderId: 7479643460, + quantity: '0.00920000', + isOrderWorking: false, + totalQuoteTradeQuantity: '0.00000000', + totalTradeQuantity: '0.00000000' + }); + + return mockUserClean; + }); + + const { setupUserWebsocket } = require('../user'); + + await setupUserWebsocket(loggerMock); + }); + + it('triggers getGridTradeLastOrder', () => { + expect(mockGridTradeLastOrder).toHaveBeenCalledWith( + loggerMock, + 'ETHUSDT', + 'buy' + ); + }); + + it('does not trigger updateGridTradeLastOrder', () => { + expect(mockUpdateGridTradeLastOrder).not.toHaveBeenCalled(); + }); + + it('does not trigger userClean', () => { + expect(mockUserClean).not.toHaveBeenCalled(); + }); + }); + describe('when last order found', () => { + beforeEach(async () => { + mockUserClean = jest.fn().mockResolvedValue(true); + + mockGridTradeLastOrder = jest + .fn() + .mockResolvedValue({ orderId: 7479643460 }); + mockUpdateGridTradeLastOrder = jest.fn().mockResolvedValue(null); + mockGetManualOrder = jest.fn().mockResolvedValue(null); + mockSaveManualOrder = jest.fn().mockResolvedValue(null); + + jest.mock('../../cronjob/trailingTradeHelper/order', () => ({ + getGridTradeLastOrder: mockGridTradeLastOrder, + updateGridTradeLastOrder: mockUpdateGridTradeLastOrder, + getManualOrder: mockGetManualOrder, + saveManualOrder: mockSaveManualOrder + })); + + binanceMock.client.ws.user = jest.fn().mockImplementationOnce(cb => { + /** + * Sample execution report + * { + * "eventType": "executionReport", + * "eventTime": 1642713283562, + * "symbol": "ETHUSDT", + * "newClientOrderId": "R4gzUYn9pQbOA3vAkgKTSw", + * "originalClientOrderId": "", + * "side": "BUY", + * "orderType": "STOP_LOSS_LIMIT", + * "timeInForce": "GTC", + * "quantity": "0.00920000", + * "price": "3248.37000000", + * "executionType": "NEW", + * "stopPrice": "3245.19000000", + * "icebergQuantity": "0.00000000", + * "orderStatus": "NEW", + * "orderRejectReason": "NONE", + * "orderId": 7479643460, + * "orderTime": 1642713283561, + * "lastTradeQuantity": "0.00000000", + * "totalTradeQuantity": "0.00000000", + * "priceLastTrade": "0.00000000", + * "commission": "0", + * "commissionAsset": null, + * "tradeId": -1, + * "isOrderWorking": false, + * "isBuyerMaker": false, + * "creationTime": 1642713283561, + * "totalQuoteTradeQuantity": "0.00000000", + * "orderListId": -1, + * "quoteOrderQuantity": "0.00000000", + * "lastQuoteTransacted": "0.00000000" + * } + */ + cb({ + eventType: 'executionReport', + eventTime: 1642713283562, + symbol: 'ETHUSDT', + side: 'BUY', + orderStatus: 'NEW', + orderType: 'STOP_LOSS_LIMIT', + stopPrice: '3245.19000000', + price: '3248.37000000', + orderId: 7479643460, + quantity: '0.00920000', + isOrderWorking: false, + totalQuoteTradeQuantity: '0.00000000', + totalTradeQuantity: '0.00000000' + }); + + return mockUserClean; + }); + + const { setupUserWebsocket } = require('../user'); + + await setupUserWebsocket(loggerMock); + }); + + it('triggers getGridTradeLastOrder', () => { + expect(mockGridTradeLastOrder).toHaveBeenCalledWith( + loggerMock, + 'ETHUSDT', + 'buy' + ); + }); + + it('triggers updateGridTradeLastOrder', () => { + expect(mockUpdateGridTradeLastOrder).toHaveBeenCalledWith( + loggerMock, + 'ETHUSDT', + 'buy', + { + cummulativeQuoteQty: '0.00000000', + executedQty: '0.00000000', + isWorking: false, + orderId: 7479643460, + origQty: '0.00920000', + price: '3248.37000000', + side: 'BUY', + status: 'NEW', + stopPrice: '3245.19000000', + type: 'STOP_LOSS_LIMIT', + updateTime: 1642713283562 + } + ); + }); + + it('does not trigger userClean', () => { + expect(mockUserClean).not.toHaveBeenCalled(); + }); + }); + + describe('when manual order not found', () => { + beforeEach(async () => { + mockUserClean = jest.fn().mockResolvedValue(true); + + mockGridTradeLastOrder = jest.fn().mockResolvedValue(null); + mockUpdateGridTradeLastOrder = jest.fn().mockResolvedValue(null); + mockGetManualOrder = jest.fn().mockResolvedValue(null); + mockSaveManualOrder = jest.fn().mockResolvedValue(null); + + jest.mock('../../cronjob/trailingTradeHelper/order', () => ({ + getGridTradeLastOrder: mockGridTradeLastOrder, + updateGridTradeLastOrder: mockUpdateGridTradeLastOrder, + getManualOrder: mockGetManualOrder, + saveManualOrder: mockSaveManualOrder + })); + + binanceMock.client.ws.user = jest.fn().mockImplementationOnce(cb => { + /** + * Sample execution report + * { + * "eventType": "executionReport", + * "eventTime": 1642713283562, + * "symbol": "ETHUSDT", + * "newClientOrderId": "R4gzUYn9pQbOA3vAkgKTSw", + * "originalClientOrderId": "", + * "side": "BUY", + * "orderType": "STOP_LOSS_LIMIT", + * "timeInForce": "GTC", + * "quantity": "0.00920000", + * "price": "3248.37000000", + * "executionType": "NEW", + * "stopPrice": "3245.19000000", + * "icebergQuantity": "0.00000000", + * "orderStatus": "NEW", + * "orderRejectReason": "NONE", + * "orderId": 7479643460, + * "orderTime": 1642713283561, + * "lastTradeQuantity": "0.00000000", + * "totalTradeQuantity": "0.00000000", + * "priceLastTrade": "0.00000000", + * "commission": "0", + * "commissionAsset": null, + * "tradeId": -1, + * "isOrderWorking": false, + * "isBuyerMaker": false, + * "creationTime": 1642713283561, + * "totalQuoteTradeQuantity": "0.00000000", + * "orderListId": -1, + * "quoteOrderQuantity": "0.00000000", + * "lastQuoteTransacted": "0.00000000" + * } + */ + cb({ + eventType: 'executionReport', + eventTime: 1642713283562, + symbol: 'ETHUSDT', + side: 'BUY', + orderStatus: 'NEW', + orderType: 'STOP_LOSS_LIMIT', + stopPrice: '3245.19000000', + price: '3248.37000000', + orderId: 7479643460, + quantity: '0.00920000', + isOrderWorking: false, + totalQuoteTradeQuantity: '0.00000000', + totalTradeQuantity: '0.00000000' + }); + + return mockUserClean; + }); + + const { setupUserWebsocket } = require('../user'); + + await setupUserWebsocket(loggerMock); + }); + + it('triggers getManualOrder', () => { + expect(mockGetManualOrder).toHaveBeenCalledWith( + loggerMock, + 'ETHUSDT', + 7479643460 + ); + }); + + it('does not trigger saveManualOrder', () => { + expect(mockSaveManualOrder).not.toHaveBeenCalled(); + }); + + it('does not trigger userClean', () => { + expect(mockUserClean).not.toHaveBeenCalled(); + }); + }); + describe('when manual order found', () => { + beforeEach(async () => { + mockUserClean = jest.fn().mockResolvedValue(true); + + mockGridTradeLastOrder = jest.fn().mockResolvedValue(null); + mockUpdateGridTradeLastOrder = jest.fn().mockResolvedValue(null); + mockGetManualOrder = jest + .fn() + .mockResolvedValue({ orderId: 7479643460 }); + mockSaveManualOrder = jest.fn().mockResolvedValue(true); + + jest.mock('../../cronjob/trailingTradeHelper/order', () => ({ + getGridTradeLastOrder: mockGridTradeLastOrder, + updateGridTradeLastOrder: mockUpdateGridTradeLastOrder, + getManualOrder: mockGetManualOrder, + saveManualOrder: mockSaveManualOrder + })); + + binanceMock.client.ws.user = jest.fn().mockImplementationOnce(cb => { + /** + * Sample execution report + * { + * "eventType": "executionReport", + * "eventTime": 1642713283562, + * "symbol": "ETHUSDT", + * "newClientOrderId": "R4gzUYn9pQbOA3vAkgKTSw", + * "originalClientOrderId": "", + * "side": "BUY", + * "orderType": "STOP_LOSS_LIMIT", + * "timeInForce": "GTC", + * "quantity": "0.00920000", + * "price": "3248.37000000", + * "executionType": "NEW", + * "stopPrice": "3245.19000000", + * "icebergQuantity": "0.00000000", + * "orderStatus": "NEW", + * "orderRejectReason": "NONE", + * "orderId": 7479643460, + * "orderTime": 1642713283561, + * "lastTradeQuantity": "0.00000000", + * "totalTradeQuantity": "0.00000000", + * "priceLastTrade": "0.00000000", + * "commission": "0", + * "commissionAsset": null, + * "tradeId": -1, + * "isOrderWorking": false, + * "isBuyerMaker": false, + * "creationTime": 1642713283561, + * "totalQuoteTradeQuantity": "0.00000000", + * "orderListId": -1, + * "quoteOrderQuantity": "0.00000000", + * "lastQuoteTransacted": "0.00000000" + * } + */ + cb({ + eventType: 'executionReport', + eventTime: 1642713283562, + symbol: 'ETHUSDT', + side: 'BUY', + orderStatus: 'NEW', + orderType: 'STOP_LOSS_LIMIT', + stopPrice: '3245.19000000', + price: '3248.37000000', + orderId: 7479643460, + quantity: '0.00920000', + isOrderWorking: false, + totalQuoteTradeQuantity: '0.00000000', + totalTradeQuantity: '0.00000000' + }); + + return mockUserClean; + }); + + const { setupUserWebsocket } = require('../user'); + + await setupUserWebsocket(loggerMock); + }); + + it('triggers getManualOrder', () => { + expect(mockGetManualOrder).toHaveBeenCalledWith( + loggerMock, + 'ETHUSDT', + 7479643460 + ); + }); + + it('triggers saveManualOrder', () => { + expect(mockSaveManualOrder).toHaveBeenCalledWith( + loggerMock, + 'ETHUSDT', + 7479643460, + { + cummulativeQuoteQty: '0.00000000', + executedQty: '0.00000000', + isWorking: false, + orderId: 7479643460, + origQty: '0.00920000', + price: '3248.37000000', + side: 'BUY', + status: 'NEW', + stopPrice: '3245.19000000', + type: 'STOP_LOSS_LIMIT', + updateTime: 1642713283562 + } + ); + }); + + it('does not trigger userClean', () => { + expect(mockUserClean).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when user clean is not null', () => { + beforeEach(async () => { + mockUserClean = jest.fn().mockResolvedValue(true); + binanceMock.client.ws.user = jest + .fn() + .mockImplementationOnce(() => mockUserClean); + + const { setupUserWebsocket } = require('../user'); + + await setupUserWebsocket(loggerMock); + + await setupUserWebsocket(loggerMock); + }); + + it('triggers user', () => { + expect(binanceMock.client.ws.user).toHaveBeenCalled(); + }); + + it('triggers userClean', () => { + expect(mockUserClean).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/app/binance/ath-candles.js b/app/binance/ath-candles.js new file mode 100644 index 00000000..e9e3fae0 --- /dev/null +++ b/app/binance/ath-candles.js @@ -0,0 +1,144 @@ +const _ = require('lodash'); +const { binance, mongo } = require('../helpers'); +const { + getConfiguration +} = require('../cronjob/trailingTradeHelper/configuration'); +const { saveCandle } = require('../cronjob/trailingTradeHelper/common'); + +let websocketATHCandlesClean = {}; + +const setupATHCandlesWebsocket = async (logger, symbols) => { + // we have to reset the opened connections in any way since we are grouping the symbols by intervals + // and not by their names + if (_.isEmpty(websocketATHCandlesClean) === false) { + logger.info('Existing opened socket for candles found, clean first'); + _.forEach(websocketATHCandlesClean, (clean, _key) => { + clean(); + }); + websocketATHCandlesClean = {}; + } + + const athSymbolsGroupedByIntervals = {}; + + // the symbols grouped by intervals to decrease the number of opened streams + // eslint-disable-next-line no-restricted-syntax + for (const symbol of symbols) { + // eslint-disable-next-line no-await-in-loop + const symbolConfiguration = await getConfiguration(logger, symbol); + + const { + buy: { + athRestriction: { + enabled: buyATHRestrictionEnabled, + candles: { interval: buyATHRestrictionCandlesInterval } + } + } + } = symbolConfiguration; + + if (buyATHRestrictionEnabled === false) { + return; + } + + if (!athSymbolsGroupedByIntervals[buyATHRestrictionCandlesInterval]) { + athSymbolsGroupedByIntervals[buyATHRestrictionCandlesInterval] = []; + } + + athSymbolsGroupedByIntervals[buyATHRestrictionCandlesInterval].push(symbol); + } + + _.forEach( + athSymbolsGroupedByIntervals, + async (symbolsGroup, candleInterval) => { + websocketATHCandlesClean[candleInterval] = binance.client.ws.candles( + symbolsGroup, + candleInterval, + candle => { + saveCandle(logger, 'trailing-trade-ath-candles', { + key: candle.symbol, + interval: candle.interval, + time: +candle.startTime, + open: +candle.open, + high: +candle.high, + low: +candle.low, + close: +candle.close, + volume: +candle.volume + }); + } + ); + } + ); +}; + +/** + * Retrieve ATH candles for symbols from Binance API + * + * @param {*} logger + * @param {string[]} symbols + */ +const syncATHCandles = async (logger, symbols) => { + await Promise.all( + symbols.map(async symbol => { + await mongo.deleteAll(logger, 'trailing-trade-ath-candles', { + key: symbol + }); + const symbolConfiguration = await getConfiguration(logger, symbol); + + const { + buy: { + athRestriction: { + enabled: buyATHRestrictionEnabled, + candles: { + interval: buyATHRestrictionCandlesInterval, + limit: buyATHRestrictionCandlesLimit + } + } + } + } = symbolConfiguration; + + const getCandles = async () => { + if (buyATHRestrictionEnabled) { + // Retrieve ath candles + logger.info( + { + debug: true, + function: 'candles', + interval: buyATHRestrictionCandlesInterval, + limit: buyATHRestrictionCandlesLimit + }, + `Retrieving ATH candles from API for ${symbol}` + ); + const athCandles = await binance.client.candles({ + symbol, + interval: buyATHRestrictionCandlesInterval, + limit: buyATHRestrictionCandlesLimit + }); + // Save ath candles for the symbol + await Promise.all( + athCandles.map(async athCandle => + saveCandle(logger, 'trailing-trade-ath-candles', { + key: symbol, + interval: buyATHRestrictionCandlesInterval, + time: +athCandle.openTime, + open: +athCandle.open, + high: +athCandle.high, + low: +athCandle.low, + close: +athCandle.close, + volume: +athCandle.volume + }) + ) + ); + } + }; + + return getCandles(); + }) + ); +}; + +const getWebsocketATHCandlesClean = () => websocketATHCandlesClean; + +module.exports = { + setupATHCandlesWebsocket, + syncATHCandles, + getWebsocketATHCandlesClean +}; diff --git a/app/binance/candles.js b/app/binance/candles.js new file mode 100644 index 00000000..183f390c --- /dev/null +++ b/app/binance/candles.js @@ -0,0 +1,118 @@ +const _ = require('lodash'); +const { binance, mongo } = require('../helpers'); +const { + getConfiguration +} = require('../cronjob/trailingTradeHelper/configuration'); +const { saveCandle } = require('../cronjob/trailingTradeHelper/common'); + +let websocketCandlesClean = {}; + +const setupCandlesWebsocket = async (logger, symbols) => { + // we have to reset the opened connections in any way since we are grouping the symbols by intervals + // and not by their names + if (_.isEmpty(websocketCandlesClean) === false) { + logger.info('Existing opened socket for candles found, clean first'); + _.forEach(websocketCandlesClean, (clean, _key) => { + clean(); + }); + websocketCandlesClean = {}; + } + + const symbolsGroupedByIntervals = {}; + + // the symbols grouped by intervals to decrease the number of opened streams + // eslint-disable-next-line no-restricted-syntax + for (const symbol of symbols) { + // eslint-disable-next-line no-await-in-loop + const symbolConfiguration = await getConfiguration(logger, symbol); + + const { + candles: { interval } + } = symbolConfiguration; + + if (!symbolsGroupedByIntervals[interval]) { + symbolsGroupedByIntervals[interval] = []; + } + + symbolsGroupedByIntervals[interval].push(symbol); + } + + _.forEach(symbolsGroupedByIntervals, (symbolsGroup, candleInterval) => { + websocketCandlesClean[candleInterval] = binance.client.ws.candles( + symbolsGroup, + candleInterval, + candle => { + saveCandle(logger, 'trailing-trade-candles', { + key: candle.symbol, + interval: candle.interval, + time: +candle.startTime, + open: +candle.open, + high: +candle.high, + low: +candle.low, + close: +candle.close, + volume: +candle.volume + }); + } + ); + }); +}; + +/** + * Retrieve ATH candles for symbols from Binance API + * + * @param {*} logger + * @param {string[]} symbols + */ +const syncCandles = async (logger, symbols) => { + await Promise.all( + symbols.map(async symbol => { + await mongo.deleteAll(logger, 'trailing-trade-candles', { + key: symbol + }); + + const symbolConfiguration = await getConfiguration(logger, symbol); + + const { + candles: { interval, limit } + } = symbolConfiguration; + + // Retrieve candles + logger.info( + { debug: true, function: 'candles', interval, limit }, + `Retrieving candles from API for ${symbol}` + ); + + const getCandles = async () => { + const candles = await binance.client.candles({ + symbol, + interval, + limit + }); + await Promise.all( + candles.map(async candle => + saveCandle(logger, 'trailing-trade-candles', { + key: symbol, + interval, + time: +candle.openTime, + open: +candle.open, + high: +candle.high, + low: +candle.low, + close: +candle.close, + volume: +candle.volume + }) + ) + ); + }; + + return getCandles(); + }) + ); +}; + +const getWebsocketCandlesClean = () => websocketCandlesClean; + +module.exports = { + setupCandlesWebsocket, + syncCandles, + getWebsocketCandlesClean +}; diff --git a/app/binance/orders.js b/app/binance/orders.js new file mode 100644 index 00000000..3705afa7 --- /dev/null +++ b/app/binance/orders.js @@ -0,0 +1,86 @@ +const _ = require('lodash'); +const { binance, cache, mongo } = require('../helpers'); +const { + getOpenOrdersFromAPI +} = require('../cronjob/trailingTradeHelper/common'); +const { + updateGridTradeLastOrder +} = require('../cronjob/trailingTradeHelper/order'); + +let openOrdersInterval; + +/** + * Retrieve open orders every x seconds + * This is just to recover open orders when an order was missed by a mistake + * + * @param {*} logger + * @param symbols + */ +const syncOpenOrders = async (logger, symbols) => { + if (openOrdersInterval) { + clearInterval(openOrdersInterval); + } + + // We do 40 seconds interval in case one of the orders missed from the websockets + openOrdersInterval = setInterval(async () => { + const openOrders = await getOpenOrdersFromAPI(logger); + + const initializedSymbolOpenOrders = _.reduce( + symbols, + (obj, symbol) => { + // eslint-disable-next-line no-param-reassign + obj[symbol] = []; + return obj; + }, + {} + ); + + const symbolOpenOrders = _.groupBy(openOrders, 'symbol'); + + const mergedOpenOrders = _.merge( + initializedSymbolOpenOrders, + symbolOpenOrders + ); + + await Promise.all( + _.map(mergedOpenOrders, (orders, symbol) => + cache.hset('trailing-trade-open-orders', symbol, JSON.stringify(orders)) + ) + ); + }, 30 * 1310); +}; + +/** + * Sync database orders on boot with binance by orderId + * This is helpful when the order executed and the bot is not on + * + * @param {*} logger + */ +const syncDatabaseOrders = async logger => { + const databaseOrders = await mongo.findAll( + logger, + 'trailing-trade-grid-trade-orders', + {} + ); + + await Promise.all( + databaseOrders.map(async databaseOrder => { + const { order } = databaseOrder; + const { symbol, orderId } = order; + + const orderResult = await binance.client.getOrder({ + symbol, + orderId + }); + + const { side } = orderResult; + + return updateGridTradeLastOrder(logger, symbol, side.toLowerCase(), { + ...order, + ...orderResult + }); + }) + ); +}; + +module.exports = { syncOpenOrders, syncDatabaseOrders }; diff --git a/app/binance/tickers.js b/app/binance/tickers.js new file mode 100644 index 00000000..c9b9d67c --- /dev/null +++ b/app/binance/tickers.js @@ -0,0 +1,88 @@ +const _ = require('lodash'); +const { binance, cache } = require('../helpers'); +const { + getAccountInfo, + getCachedExchangeSymbols +} = require('../cronjob/trailingTradeHelper/common'); +const { executeTrailingTrade } = require('../cronjob'); + +let websocketTickersClean = {}; + +const setupTickersWebsocket = async (logger, symbols) => { + const accountInfo = await getAccountInfo(logger); + + const cachedExchangeSymbols = await getCachedExchangeSymbols(logger); + + const monitoringSymbols = _.cloneDeep(symbols); + + // we are adding ${symbol}BTC to our monitoring symbols to support + // dust transfer feature, and we will not use them for anything else + accountInfo.balances.reduce((acc, b) => { + const symbol = `${b.asset}BTC`; + // Make sure the symbol existing in Binance. Otherwise, just ignore. + if ( + cachedExchangeSymbols[symbol] !== undefined && + acc.includes(symbol) === false + ) { + acc.push(symbol); + } + return acc; + }, monitoringSymbols); + + // eslint-disable-next-line no-restricted-syntax + for (const monitoringSymbol of monitoringSymbols) { + if (monitoringSymbol in websocketTickersClean) { + logger.info( + `Existing opened stream for ${monitoringSymbol} ticker found, clean first` + ); + websocketTickersClean[monitoringSymbol](); + } + + websocketTickersClean[monitoringSymbol] = binance.client.ws.miniTicker( + monitoringSymbol, + ticker => { + const { eventType, eventTime, curDayClose: close, symbol } = ticker; + // // Record last received date/time + // lastReceivedAt = moment(); + + // Save latest candle for the symbol + cache.hset( + 'trailing-trade-symbols', + `${symbol}-latest-candle`, + JSON.stringify({ + eventType, + eventTime, + symbol, + close + }) + ); + + const canExecuteTrailingTrade = symbols.includes(monitoringSymbol); + + logger.info({ ticker, canExecuteTrailingTrade }, 'Received new ticker'); + + if (canExecuteTrailingTrade) { + executeTrailingTrade(logger, monitoringSymbol); + } + } + ); + } +}; + +const getWebsocketTickersClean = () => websocketTickersClean; + +const refreshTickersClean = logger => { + if (_.isEmpty(websocketTickersClean) === false) { + logger.info('Existing opened socket for tickers found, clean first'); + _.forEach(websocketTickersClean, (clean, _key) => { + clean(); + }); + websocketTickersClean = {}; + } +}; + +module.exports = { + setupTickersWebsocket, + getWebsocketTickersClean, + refreshTickersClean +}; diff --git a/app/binance/user.js b/app/binance/user.js new file mode 100644 index 00000000..7cc0be58 --- /dev/null +++ b/app/binance/user.js @@ -0,0 +1,107 @@ +const _ = require('lodash'); + +const { binance } = require('../helpers'); + +const { + updateAccountInfo, + getAccountInfoFromAPI +} = require('../cronjob/trailingTradeHelper/common'); + +const { + getGridTradeLastOrder, + updateGridTradeLastOrder, + getManualOrder, + saveManualOrder +} = require('../cronjob/trailingTradeHelper/order'); + +let userClean; + +const setupUserWebsocket = async logger => { + if (userClean) { + logger.info('Existing opened socket for user found, clean first'); + userClean(); + } + + userClean = await binance.client.ws.user(evt => { + const { eventType } = evt; + + logger.info({ evt }, 'Received new user activity'); + + if (['balanceUpdate', 'account'].includes(eventType)) { + getAccountInfoFromAPI(logger); + } + + if (eventType === 'outboundAccountPosition') { + const { balances, lastAccountUpdate } = evt; + updateAccountInfo(logger, balances, lastAccountUpdate); + } + + if (eventType === 'executionReport') { + const { + eventTime, + symbol, + side, + orderStatus, + orderType, + stopPrice, + price, + orderId, + quantity, + isOrderWorking, + totalQuoteTradeQuantity, + totalTradeQuantity + } = evt; + logger.info({ evt }, 'Received new report'); + + const checkLastOrder = async () => { + const lastOrder = await getGridTradeLastOrder( + logger, + symbol, + side.toLowerCase() + ); + + if (_.isEmpty(lastOrder) === false) { + await updateGridTradeLastOrder(logger, symbol, side.toLowerCase(), { + ...lastOrder, + status: orderStatus, + type: orderType, + side, + stopPrice, + price, + origQty: quantity, + cummulativeQuoteQty: totalQuoteTradeQuantity, + executedQty: totalTradeQuantity, + isWorking: isOrderWorking, + updateTime: eventTime + }); + } + }; + + checkLastOrder(); + + const checkManualOrder = async () => { + const manualOrder = await getManualOrder(logger, symbol, orderId); + + if (_.isEmpty(manualOrder) === false) { + await saveManualOrder(logger, symbol, orderId, { + ...manualOrder, + status: orderStatus, + type: orderType, + side, + stopPrice, + price, + origQty: quantity, + cummulativeQuoteQty: totalQuoteTradeQuantity, + executedQty: totalTradeQuantity, + isWorking: isOrderWorking, + updateTime: eventTime + }); + } + }; + + checkManualOrder(); + } + }); +}; + +module.exports = { setupUserWebsocket }; diff --git a/app/cronjob/__tests__/trailingTrade.test.js b/app/cronjob/__tests__/trailingTrade.test.js index 564314b6..faf7c7f3 100644 --- a/app/cronjob/__tests__/trailingTrade.test.js +++ b/app/cronjob/__tests__/trailingTrade.test.js @@ -9,8 +9,6 @@ describe('trailingTrade', () => { let mockSlackSendMessage; let mockConfigGet; - let mockGetGlobalConfiguration; - let mockCacheExchangeSymbols; let mockGetAccountInfo; let mockLockSymbol; @@ -58,7 +56,7 @@ describe('trailingTrade', () => { }); describe('without any error', () => { - beforeEach(async () => { + beforeEach(() => { jest.clearAllMocks().resetModules(); mockConfigGet = jest.fn(key => { @@ -78,10 +76,6 @@ describe('trailingTrade', () => { mockIsSymbolLocked = jest.fn().mockResolvedValue(false); mockUnlockSymbol = jest.fn().mockResolvedValue(true); - mockGetGlobalConfiguration = jest.fn().mockResolvedValue({ - symbols: ['BTCUSDT', 'ETHUSDT', 'LTCUSDT'] - }); - mockCacheExchangeSymbols = jest.fn().mockResolvedValue(true); mockGetAccountInfo = jest.fn().mockResolvedValue({ @@ -260,10 +254,6 @@ describe('trailingTrade', () => { } })); - jest.mock('../trailingTradeHelper/configuration', () => ({ - getGlobalConfiguration: mockGetGlobalConfiguration - })); - jest.mock('../trailingTradeHelper/common', () => ({ cacheExchangeSymbols: mockCacheExchangeSymbols, getAccountInfo: mockGetAccountInfo, @@ -292,135 +282,150 @@ describe('trailingTrade', () => { removeLastBuyPrice: mockRemoveLastBuyPrice, saveDataToCache: mockSaveDataToCache })); - - const { execute: trailingTradeExecute } = require('../trailingTrade'); - - await trailingTradeExecute(logger); }); - ['BTCUSDT', 'ETHUSDT', 'LTCUSDT'].forEach(symbol => { - it(`triggers isSymbolLocked - ${symbol}`, () => { - expect(mockIsSymbolLocked).toHaveBeenCalledWith(logger, symbol); + describe('execute trailing trade for BTCUSDT', () => { + beforeEach(async () => { + const { execute: trailingTradeExecute } = require('../trailingTrade'); + await trailingTradeExecute(logger, 'BTCUSDT'); }); - it(`triggers lockSymbol - ${symbol}`, () => { - expect(mockLockSymbol).toHaveBeenCalledWith(logger, symbol); + it(`triggers isSymbolLocked - BTCUSDT`, () => { + expect(mockIsSymbolLocked).toHaveBeenCalledWith(logger, 'BTCUSDT'); }); - it(`triggers unlockSymbol - ${symbol}`, () => { - expect(mockUnlockSymbol).toHaveBeenCalledWith(logger, symbol); + it('returns expected result for BTCUSDT', () => { + expect(mockLoggerInfo).toHaveBeenCalledWith( + { + symbol: 'BTCUSDT', + data: { + symbol: 'BTCUSDT', + isLocked: false, + featureToggle: { feature1Enabled: true }, + lastCandle: { got: 'lowest value' }, + accountInfo: { account: 'info' }, + symbolConfiguration: { symbol: 'configuration data' }, + indicators: { some: 'value' }, + symbolInfo: { symbol: 'info' }, + openOrders: [{ orderId: 'order-id-BTCUSDT', symbol: 'BTCUSDT' }], + action: 'determined', + baseAssetBalance: { baseAsset: 'balance' }, + quoteAssetBalance: { quoteAsset: 'balance' }, + buy: { should: 'buy?', actioned: 'yes' }, + sell: { should: 'sell?', actioned: 'yes' }, + overrideAction: { action: 'override-action' }, + ensureManualOrder: { ensured: 'manual-buy-order' }, + ensureGridTradeOrder: { ensured: 'grid-trade' }, + handled: 'open-orders', + placeManualTrade: { placed: 'manual-trade' }, + cancelOrder: { cancelled: 'existing-order' }, + stopLoss: 'processed', + removed: 'last-buy-price', + order: {}, + canDisable: true, + saveToCache: true, + saved: 'data-to-cache' + } + }, + 'TrailingTrade: Finish process...' + ); }); }); - it('returns expected result for BTCUSDT', () => { - expect(mockLoggerInfo).toHaveBeenCalledWith( - { - symbol: 'BTCUSDT', - data: { - symbol: 'BTCUSDT', - isLocked: false, - featureToggle: { feature1Enabled: true }, - lastCandle: { got: 'lowest value' }, - accountInfo: { account: 'info' }, - symbolConfiguration: { symbol: 'configuration data' }, - indicators: { some: 'value' }, - symbolInfo: { symbol: 'info' }, - openOrders: [{ orderId: 'order-id-BTCUSDT', symbol: 'BTCUSDT' }], - action: 'determined', - baseAssetBalance: { baseAsset: 'balance' }, - quoteAssetBalance: { quoteAsset: 'balance' }, - buy: { should: 'buy?', actioned: 'yes' }, - sell: { should: 'sell?', actioned: 'yes' }, - overrideAction: { action: 'override-action' }, - ensureManualOrder: { ensured: 'manual-buy-order' }, - ensureGridTradeOrder: { ensured: 'grid-trade' }, - handled: 'open-orders', - placeManualTrade: { placed: 'manual-trade' }, - cancelOrder: { cancelled: 'existing-order' }, - stopLoss: 'processed', - removed: 'last-buy-price', - order: {}, - canDisable: true, - saveToCache: true, - saved: 'data-to-cache' - } - }, - 'TrailingTrade: Finish process...' - ); - }); + describe('execute trailing trade for ETHUSDT', () => { + beforeEach(async () => { + const { execute: trailingTradeExecute } = require('../trailingTrade'); + await trailingTradeExecute(logger, 'ETHUSDT'); + }); - it('returns expected result for ETHUSDT', () => { - expect(mockLoggerInfo).toHaveBeenCalledWith( - { - symbol: 'ETHUSDT', - data: { + it(`triggers isSymbolLocked - ETHUSDT`, () => { + expect(mockIsSymbolLocked).toHaveBeenCalledWith(logger, 'ETHUSDT'); + }); + + it('returns expected result for ETHUSDT', () => { + expect(mockLoggerInfo).toHaveBeenCalledWith( + { symbol: 'ETHUSDT', - isLocked: false, - featureToggle: { feature1Enabled: true }, - lastCandle: { got: 'lowest value' }, - accountInfo: { account: 'info' }, - symbolConfiguration: { symbol: 'configuration data' }, - indicators: { some: 'value' }, - symbolInfo: { symbol: 'info' }, - openOrders: [{ orderId: 'order-id-ETHUSDT', symbol: 'ETHUSDT' }], - action: 'determined', - baseAssetBalance: { baseAsset: 'balance' }, - quoteAssetBalance: { quoteAsset: 'balance' }, - buy: { should: 'buy?', actioned: 'yes' }, - sell: { should: 'sell?', actioned: 'yes' }, - overrideAction: { action: 'override-action' }, - ensureManualOrder: { ensured: 'manual-buy-order' }, - ensureGridTradeOrder: { ensured: 'grid-trade' }, - handled: 'open-orders', - placeManualTrade: { placed: 'manual-trade' }, - cancelOrder: { cancelled: 'existing-order' }, - stopLoss: 'processed', - removed: 'last-buy-price', - canDisable: true, - order: {}, - saveToCache: true, - saved: 'data-to-cache' - } - }, - 'TrailingTrade: Finish process...' - ); + data: { + symbol: 'ETHUSDT', + isLocked: false, + featureToggle: { feature1Enabled: true }, + lastCandle: { got: 'lowest value' }, + accountInfo: { account: 'info' }, + symbolConfiguration: { symbol: 'configuration data' }, + indicators: { some: 'value' }, + symbolInfo: { symbol: 'info' }, + openOrders: [{ orderId: 'order-id-ETHUSDT', symbol: 'ETHUSDT' }], + action: 'determined', + baseAssetBalance: { baseAsset: 'balance' }, + quoteAssetBalance: { quoteAsset: 'balance' }, + buy: { should: 'buy?', actioned: 'yes' }, + sell: { should: 'sell?', actioned: 'yes' }, + overrideAction: { action: 'override-action' }, + ensureManualOrder: { ensured: 'manual-buy-order' }, + ensureGridTradeOrder: { ensured: 'grid-trade' }, + handled: 'open-orders', + placeManualTrade: { placed: 'manual-trade' }, + cancelOrder: { cancelled: 'existing-order' }, + stopLoss: 'processed', + removed: 'last-buy-price', + canDisable: true, + order: {}, + saveToCache: true, + saved: 'data-to-cache' + } + }, + 'TrailingTrade: Finish process...' + ); + }); }); - it('returns expected result for LTCUSDT', () => { - expect(mockLoggerInfo).toHaveBeenCalledWith( - { - symbol: 'LTCUSDT', - data: { + describe('execute trailing trade for LTCUSDT', () => { + beforeEach(async () => { + const { execute: trailingTradeExecute } = require('../trailingTrade'); + await trailingTradeExecute(logger, 'LTCUSDT'); + }); + + it(`triggers isSymbolLocked - LTCUSDT`, () => { + expect(mockIsSymbolLocked).toHaveBeenCalledWith(logger, 'LTCUSDT'); + }); + + it('returns expected result for LTCUSDT', async () => { + expect(mockLoggerInfo).toHaveBeenCalledWith( + { symbol: 'LTCUSDT', - isLocked: false, - featureToggle: { feature1Enabled: true }, - lastCandle: { got: 'lowest value' }, - accountInfo: { account: 'info' }, - symbolConfiguration: { symbol: 'configuration data' }, - indicators: { some: 'value' }, - symbolInfo: { symbol: 'info' }, - openOrders: [{ orderId: 'order-id-LTCUSDT', symbol: 'LTCUSDT' }], - action: 'determined', - baseAssetBalance: { baseAsset: 'balance' }, - quoteAssetBalance: { quoteAsset: 'balance' }, - buy: { should: 'buy?', actioned: 'yes' }, - sell: { should: 'sell?', actioned: 'yes' }, - overrideAction: { action: 'override-action' }, - ensureManualOrder: { ensured: 'manual-buy-order' }, - ensureGridTradeOrder: { ensured: 'grid-trade' }, - handled: 'open-orders', - placeManualTrade: { placed: 'manual-trade' }, - cancelOrder: { cancelled: 'existing-order' }, - stopLoss: 'processed', - removed: 'last-buy-price', - canDisable: true, - order: {}, - saveToCache: true, - saved: 'data-to-cache' - } - }, - 'TrailingTrade: Finish process...' - ); + data: { + symbol: 'LTCUSDT', + isLocked: false, + featureToggle: { feature1Enabled: true }, + lastCandle: { got: 'lowest value' }, + accountInfo: { account: 'info' }, + symbolConfiguration: { symbol: 'configuration data' }, + indicators: { some: 'value' }, + symbolInfo: { symbol: 'info' }, + openOrders: [{ orderId: 'order-id-LTCUSDT', symbol: 'LTCUSDT' }], + action: 'determined', + baseAssetBalance: { baseAsset: 'balance' }, + quoteAssetBalance: { quoteAsset: 'balance' }, + buy: { should: 'buy?', actioned: 'yes' }, + sell: { should: 'sell?', actioned: 'yes' }, + overrideAction: { action: 'override-action' }, + ensureManualOrder: { ensured: 'manual-buy-order' }, + ensureGridTradeOrder: { ensured: 'grid-trade' }, + handled: 'open-orders', + placeManualTrade: { placed: 'manual-trade' }, + cancelOrder: { cancelled: 'existing-order' }, + stopLoss: 'processed', + removed: 'last-buy-price', + canDisable: true, + order: {}, + saveToCache: true, + saved: 'data-to-cache' + } + }, + 'TrailingTrade: Finish process...' + ); + }); }); }); @@ -445,10 +450,6 @@ describe('trailingTrade', () => { mockIsSymbolLocked = jest.fn().mockResolvedValue(true); mockUnlockSymbol = jest.fn().mockResolvedValue(true); - mockGetGlobalConfiguration = jest.fn().mockResolvedValue({ - symbols: ['BTCUSDT', 'ETHUSDT', 'LTCUSDT'] - }); - mockCacheExchangeSymbols = jest.fn().mockResolvedValue(true); mockGetAccountInfo = jest.fn().mockResolvedValue({ @@ -627,10 +628,6 @@ describe('trailingTrade', () => { } })); - jest.mock('../trailingTradeHelper/configuration', () => ({ - getGlobalConfiguration: mockGetGlobalConfiguration - })); - jest.mock('../trailingTradeHelper/common', () => ({ cacheExchangeSymbols: mockCacheExchangeSymbols, getAccountInfo: mockGetAccountInfo, @@ -659,64 +656,67 @@ describe('trailingTrade', () => { removeLastBuyPrice: mockRemoveLastBuyPrice, saveDataToCache: mockSaveDataToCache })); - - const { execute: trailingTradeExecute } = require('../trailingTrade'); - - await trailingTradeExecute(logger); }); - ['BTCUSDT', 'ETHUSDT', 'LTCUSDT'].forEach(symbol => { - it(`triggers isSymbolLocked - ${symbol}`, () => { - expect(mockIsSymbolLocked).toHaveBeenCalledWith(logger, symbol); + describe('execute trailing trade for BTCUSDT', () => { + beforeEach(async () => { + const { execute: trailingTradeExecute } = require('../trailingTrade'); + await trailingTradeExecute(logger, 'BTCUSDT'); }); - it(`does not trigger lockSymbol - ${symbol}`, () => { - expect(mockLockSymbol).not.toHaveBeenCalled(); + it(`triggers isSymbolLocked - BTCUSDT`, () => { + expect(mockIsSymbolLocked).toHaveBeenCalledWith(logger, 'BTCUSDT'); }); - - it(`does not trigger unlockSymbol - ${symbol}`, () => { - expect(mockUnlockSymbol).not.toHaveBeenCalled(); + it('returns expected result for BTCUSDT', async () => { + expect(mockLoggerInfo).toHaveBeenCalledWith( + { + symbol: 'BTCUSDT', + data: { + symbol: 'BTCUSDT', + isLocked: true, + featureToggle: { feature1Enabled: false }, + accountInfo: { account: 'info' }, + symbolConfiguration: { symbol: 'configuration data' }, + symbolInfo: { symbol: 'info' }, + overrideAction: { action: 'override-action' }, + ensureManualOrder: { ensured: 'manual-buy-order' }, + ensureGridTradeOrder: { ensured: 'grid-trade' }, + baseAssetBalance: { baseAsset: 'balance' }, + quoteAssetBalance: { quoteAsset: 'balance' }, + openOrders: [{ orderId: 'order-id-BTCUSDT', symbol: 'BTCUSDT' }], + lastCandle: { got: 'lowest value' }, + indicators: { some: 'value' }, + buy: { should: 'buy?', actioned: 'yes' }, + sell: { should: 'sell?', actioned: 'yes' }, + handled: 'open-orders', + action: 'determined', + placeManualTrade: { placed: 'manual-trade' }, + cancelOrder: { cancelled: 'existing-order' }, + stopLoss: 'processed', + removed: 'last-buy-price', + order: {}, + canDisable: true, + saveToCache: true, + saved: 'data-to-cache' + } + }, + 'TrailingTrade: Finish process...' + ); }); }); + }); - it('returns expected result for BTCUSDT', () => { - expect(mockLoggerInfo).toHaveBeenCalledWith( - { - symbol: 'BTCUSDT', - data: { - symbol: 'BTCUSDT', - isLocked: true, - featureToggle: { feature1Enabled: false }, - accountInfo: { account: 'info' }, - symbolConfiguration: { symbol: 'configuration data' }, - symbolInfo: { symbol: 'info' }, - overrideAction: { action: 'override-action' }, - ensureManualOrder: { ensured: 'manual-buy-order' }, - ensureGridTradeOrder: { ensured: 'grid-trade' }, - baseAssetBalance: { baseAsset: 'balance' }, - quoteAssetBalance: { quoteAsset: 'balance' }, - openOrders: [{ orderId: 'order-id-BTCUSDT', symbol: 'BTCUSDT' }], - lastCandle: { got: 'lowest value' }, - indicators: { some: 'value' }, - buy: { should: 'buy?', actioned: 'yes' }, - sell: { should: 'sell?', actioned: 'yes' }, - handled: 'open-orders', - action: 'determined', - placeManualTrade: { placed: 'manual-trade' }, - cancelOrder: { cancelled: 'existing-order' }, - stopLoss: 'processed', - removed: 'last-buy-price', - order: {}, - canDisable: true, - saveToCache: true, - saved: 'data-to-cache' - } - }, - 'TrailingTrade: Finish process...' - ); + describe('execute trailing trade for ETHUSDT', () => { + beforeEach(async () => { + const { execute: trailingTradeExecute } = require('../trailingTrade'); + await trailingTradeExecute(logger, 'ETHUSDT'); }); - it('returns expected result for ETHUSDT', () => { + it(`triggers isSymbolLocked - ETHUSDT`, () => { + expect(mockIsSymbolLocked).toHaveBeenCalledWith(logger, 'ETHUSDT'); + }); + + it('returns expected result for ETHUSDT', async () => { expect(mockLoggerInfo).toHaveBeenCalledWith( { symbol: 'ETHUSDT', @@ -752,8 +752,19 @@ describe('trailingTrade', () => { 'TrailingTrade: Finish process...' ); }); + }); + + describe('execute trailing trade for LTCUSDT', () => { + beforeEach(async () => { + const { execute: trailingTradeExecute } = require('../trailingTrade'); + await trailingTradeExecute(logger, 'LTCUSDT'); + }); - it('returns expected result for LTCUSDT', () => { + it(`triggers isSymbolLocked - LTCUSDT`, () => { + expect(mockIsSymbolLocked).toHaveBeenCalledWith(logger, 'LTCUSDT'); + }); + + it('returns expected result for ETHUSDT', () => { expect(mockLoggerInfo).toHaveBeenCalledWith( { symbol: 'LTCUSDT', @@ -820,10 +831,6 @@ describe('trailingTrade', () => { mockRemoveLastBuyPrice = jest.fn().mockResolvedValue(true); mockSaveDataToCache = jest.fn().mockResolvedValue(true); - jest.mock('../trailingTradeHelper/configuration', () => ({ - getGlobalConfiguration: mockGetGlobalConfiguration - })); - jest.mock('../trailingTradeHelper/common', () => ({ cacheExchangeSymbols: mockCacheExchangeSymbols, getAccountInfo: mockGetAccountInfo, @@ -905,7 +912,7 @@ describe('trailingTrade', () => { get: mockConfigGet })); - mockGetGlobalConfiguration = jest.fn().mockRejectedValueOnce( + mockGetAccountInfo = jest.fn().mockRejectedValueOnce( new (class CustomError extends Error { constructor() { super(); @@ -917,7 +924,7 @@ describe('trailingTrade', () => { const { execute: trailingTradeExecute } = require('../trailingTrade'); - await trailingTradeExecute(logger); + await trailingTradeExecute(logger, 'BTCUSDT'); }); if (errorInfo.sendSlack) { @@ -940,22 +947,22 @@ describe('trailingTrade', () => { get: mockConfigGet })); - mockGetGlobalConfiguration = jest.fn().mockRejectedValueOnce( + mockIsSymbolLocked = jest.fn().mockRejectedValueOnce( new (class CustomError extends Error { constructor() { super(); this.code = 500; - this.message = `redlock:lock-XRPBUSD`; + this.message = `redlock:bot-lock:XRPBUSD`; } })() ); const { execute: trailingTradeExecute } = require('../trailingTrade'); - await trailingTradeExecute(logger); + await trailingTradeExecute(logger, 'XRPBUSD'); }); - it('does not trigger slack.sendMessagage', () => { + it('do not trigger slack.sendMessagage', () => { expect(mockSlackSendMessage).not.toHaveBeenCalled(); }); }); diff --git a/app/cronjob/__tests__/trailingTradeIndicator.test.js b/app/cronjob/__tests__/trailingTradeIndicator.test.js index 6904ce75..2545d291 100644 --- a/app/cronjob/__tests__/trailingTradeIndicator.test.js +++ b/app/cronjob/__tests__/trailingTradeIndicator.test.js @@ -14,9 +14,6 @@ describe('trailingTradeIndicator', () => { let mockGetSymbolConfiguration; let mockGetSymbolInfo; let mockGetOverrideAction; - let mockGetAccountInfo; - let mockGetIndicators; - let mockGetOpenOrders; let mockExecuteDustTransfer; let mockGetClosedTrades; let mockGetOrderStats; @@ -100,31 +97,6 @@ describe('trailingTradeIndicator', () => { } })); - mockGetAccountInfo = jest.fn().mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - accountInfo: { - account: 'information' - } - } - })); - - mockGetIndicators = jest.fn().mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - indicators: { - some: 'value' - } - } - })); - - mockGetOpenOrders = jest.fn().mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - openOrders: [{ orderId: 1 }] - } - })); - mockExecuteDustTransfer = jest .fn() .mockImplementation((_logger, rawData) => ({ @@ -168,9 +140,6 @@ describe('trailingTradeIndicator', () => { getSymbolConfiguration: mockGetSymbolConfiguration, getSymbolInfo: mockGetSymbolInfo, getOverrideAction: mockGetOverrideAction, - getAccountInfo: mockGetAccountInfo, - getIndicators: mockGetIndicators, - getOpenOrders: mockGetOpenOrders, executeDustTransfer: mockExecuteDustTransfer, getClosedTrades: mockGetClosedTrades, getOrderStats: mockGetOrderStats, @@ -236,9 +205,6 @@ describe('trailingTradeIndicator', () => { symbol: 'BTCUSDT', symbolConfiguration: { symbol: 'configuration data' }, symbolInfo: { some: 'info' }, - accountInfo: { account: 'information' }, - indicators: { some: 'value' }, - openOrders: [{ orderId: 1 }], overrideParams: { param: 'overrided' }, quoteAssetStats: {}, apiLimit: { start: 10, end: 10 }, diff --git a/app/cronjob/trailingTrade.js b/app/cronjob/trailingTrade.js index d96c2693..300a2608 100644 --- a/app/cronjob/trailingTrade.js +++ b/app/cronjob/trailingTrade.js @@ -1,15 +1,9 @@ const moment = require('moment'); const config = require('config'); -const { - getGlobalConfiguration -} = require('./trailingTradeHelper/configuration'); const { - cacheExchangeSymbols, getAccountInfo, - lockSymbol, isSymbolLocked, - unlockSymbol, getAPILimit } = require('./trailingTradeHelper/common'); @@ -34,153 +28,136 @@ const { } = require('./trailingTrade/steps'); const { slack } = require('../helpers'); -const execute = async logger => { - try { - // Retrieve global configuration - const globalConfiguration = await getGlobalConfiguration(logger); - - // Retrieve exchange symbols and cache it - await cacheExchangeSymbols(logger, globalConfiguration); +const execute = async (rawLogger, symbol) => { + const logger = rawLogger.child({ jobName: 'trailingTrade' }); + try { // Retrieve account info from cache const accountInfo = await getAccountInfo(logger); // Retrieve feature toggles const featureToggle = config.get('featureToggle'); - await Promise.all( - globalConfiguration.symbols.map(async symbol => { - logger.info({ symbol }, '▶ TrailingTrade: Start process...'); - - // Check if the symbol is locked, if it is locked, it means the symbol is still processing. - const isLocked = await isSymbolLocked(logger, symbol); - - if (isLocked === false) { - await lockSymbol(logger, symbol); - } - - // Define sekeleton of data structure - let data = { - symbol, - isLocked, - featureToggle, - lastCandle: {}, - accountInfo, - symbolConfiguration: {}, - indicators: {}, - symbolInfo: {}, - openOrders: [], - action: 'not-determined', - baseAssetBalance: {}, - quoteAssetBalance: {}, - buy: {}, - sell: {}, - order: {}, - canDisable: true, - saveToCache: true - }; - - // eslint-disable-next-line no-restricted-syntax - for (const { stepName, stepFunc } of [ - { - stepName: 'get-symbol-configuration', - stepFunc: getSymbolConfiguration - }, - { - stepName: 'get-symbol-info', - stepFunc: getSymbolInfo - }, - { - stepName: 'ensure-manual-order', - stepFunc: ensureManualOrder - }, - { - stepName: 'ensure-grid-trade-order-executed', - stepFunc: ensureGridTradeOrderExecuted - }, - { - stepName: 'get-balances', - stepFunc: getBalances - }, - { - stepName: 'get-open-orders', - stepFunc: getOpenOrders - }, - { - stepName: 'get-indicators', - stepFunc: getIndicators - }, - { - stepName: 'get-override-action', - stepFunc: getOverrideAction - }, - { - stepName: 'handle-open-orders', - stepFunc: handleOpenOrders - }, - // In case account information is updated, get balance again. - { - stepName: 'get-balances', - stepFunc: getBalances - }, - { - stepName: 'determine-action', - stepFunc: determineAction - }, - { - stepName: 'place-manual-order', - stepFunc: placeManualTrade - }, - { - stepName: 'cancel-order', - stepFunc: cancelOrder - }, - { - stepName: 'place-buy-order', - stepFunc: placeBuyOrder - }, - { - stepName: 'place-sell-order', - stepFunc: placeSellOrder - }, - { - stepName: 'place-sell-stop-loss-order', - stepFunc: placeSellStopLossOrder - }, - // In case account information is updated, get balance again. - { - stepName: 'get-balances', - stepFunc: getBalances - }, - { - stepName: 'remove-last-buy-price', - stepFunc: removeLastBuyPrice - }, - { - stepName: 'save-data-to-cache', - stepFunc: saveDataToCache - } - ]) { - const stepLogger = logger.child({ stepName, symbol: data.symbol }); - - stepLogger.info({ data }, `Start step - ${stepName}`); - - // eslint-disable-next-line no-await-in-loop - data = await stepFunc(stepLogger, data); - - stepLogger.info({ data }, `Finish step - ${stepName}`); - } - - // Unlock symbol for processing if it is not locked by another process - if (isLocked === false) { - await unlockSymbol(logger, symbol); - } - - logger.info({ symbol }, '⏹ TrailingTrade: Finish process (Debug)...'); - - logger.info({ symbol, data }, 'TrailingTrade: Finish process...'); - }) - ); + logger.info({ debug: true, symbol }, '▶ TrailingTrade: Start process...'); + + // Check if the symbol is locked, if it is locked, it means the symbol is still processing. + const isLocked = await isSymbolLocked(logger, symbol); + + // Define skeleton of data structure + let data = { + symbol, + isLocked, + featureToggle, + lastCandle: {}, + accountInfo, + symbolConfiguration: {}, + indicators: {}, + symbolInfo: {}, + openOrders: [], + action: 'not-determined', + baseAssetBalance: {}, + quoteAssetBalance: {}, + buy: {}, + sell: {}, + order: {}, + canDisable: true, + saveToCache: true + }; + + // eslint-disable-next-line no-restricted-syntax + for (const { stepName, stepFunc } of [ + { + stepName: 'get-symbol-configuration', + stepFunc: getSymbolConfiguration + }, + { + stepName: 'get-symbol-info', + stepFunc: getSymbolInfo + }, + { + stepName: 'ensure-manual-order', + stepFunc: ensureManualOrder + }, + { + stepName: 'ensure-grid-trade-order-executed', + stepFunc: ensureGridTradeOrderExecuted + }, + { + stepName: 'get-balances', + stepFunc: getBalances + }, + { + stepName: 'get-open-orders', + stepFunc: getOpenOrders + }, + { + stepName: 'get-indicators', + stepFunc: getIndicators + }, + { + stepName: 'get-override-action', + stepFunc: getOverrideAction + }, + { + stepName: 'handle-open-orders', + stepFunc: handleOpenOrders + }, + // In case account information is updated, get balance again. + { + stepName: 'get-balances', + stepFunc: getBalances + }, + { + stepName: 'determine-action', + stepFunc: determineAction + }, + { + stepName: 'place-manual-order', + stepFunc: placeManualTrade + }, + { + stepName: 'cancel-order', + stepFunc: cancelOrder + }, + { + stepName: 'place-buy-order', + stepFunc: placeBuyOrder + }, + { + stepName: 'place-sell-order', + stepFunc: placeSellOrder + }, + { + stepName: 'place-sell-stop-loss-order', + stepFunc: placeSellStopLossOrder + }, + // In case account information is updated, get balance again. + { + stepName: 'get-balances', + stepFunc: getBalances + }, + { + stepName: 'remove-last-buy-price', + stepFunc: removeLastBuyPrice + }, + { + stepName: 'save-data-to-cache', + stepFunc: saveDataToCache + } + ]) { + const stepLogger = logger.child({ stepName, symbol: data.symbol }); + + stepLogger.info({ data }, `Start step - ${stepName}`); + + // eslint-disable-next-line no-await-in-loop + data = await stepFunc(stepLogger, data); + + stepLogger.info({ data }, `Finish step - ${stepName}`); + } + + logger.info({ symbol }, '⏹ TrailingTrade: Finish process (Debug)...'); + + logger.info({ symbol, data }, 'TrailingTrade: Finish process...'); } catch (err) { // For the redlock fail if (err.message.includes('redlock')) { @@ -188,10 +165,13 @@ const execute = async logger => { return; } - logger.error({ err, errorCode: err.code }, `⚠ Execution failed.`); + logger.error( + { err, errorCode: err.code, debug: true, symbol, saveLog: true }, + `⚠ Execution failed.` + ); if ( err.code === -1001 || - err.code === -1021 || // Timestamp for this request is outside of the recvWindow + err.code === -1021 || // Timestamp for this request is outside the recvWindow err.code === 'ECONNRESET' || err.code === 'ECONNREFUSED' ) { diff --git a/app/cronjob/trailingTrade/step/__tests__/cancel-order.test.js b/app/cronjob/trailingTrade/step/__tests__/cancel-order.test.js index 0ea558db..d45bfd0c 100644 --- a/app/cronjob/trailingTrade/step/__tests__/cancel-order.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/cancel-order.test.js @@ -10,7 +10,7 @@ describe('cancel-order.js', () => { let mockGetAPILimit; let mockGetAndCacheOpenOrdersForSymbol; - let mockGetAccountInfoFromAPI; + let mockGetAccountInfo; let mockDeleteManualOrder; @@ -29,7 +29,7 @@ describe('cancel-order.js', () => { binanceMock.client.cancelOrder = jest.fn().mockResolvedValue(true); mockGetAPILimit = jest.fn().mockReturnValue(10); mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); PubSubMock.publish = jest.fn().mockResolvedValue(true); @@ -46,7 +46,7 @@ describe('cancel-order.js', () => { jest.mock('../../../trailingTradeHelper/common', () => ({ getAPILimit: mockGetAPILimit, getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI + getAccountInfo: mockGetAccountInfo })); const step = require('../cancel-order'); @@ -74,7 +74,7 @@ describe('cancel-order.js', () => { jest.mock('../../../trailingTradeHelper/common', () => ({ getAPILimit: mockGetAPILimit, getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI + getAccountInfo: mockGetAccountInfo })); const step = require('../cancel-order'); @@ -114,7 +114,7 @@ describe('cancel-order.js', () => { jest.mock('../../../trailingTradeHelper/common', () => ({ getAPILimit: mockGetAPILimit, getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI + getAccountInfo: mockGetAccountInfo })); const step = require('../cancel-order'); @@ -224,7 +224,7 @@ describe('cancel-order.js', () => { jest.mock('../../../trailingTradeHelper/common', () => ({ getAPILimit: mockGetAPILimit, getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI + getAccountInfo: mockGetAccountInfo })); const step = require('../cancel-order'); diff --git a/app/cronjob/trailingTrade/step/__tests__/ensure-grid-trade-order-executed.test.js b/app/cronjob/trailingTrade/step/__tests__/ensure-grid-trade-order-executed.test.js index 39ed3209..2144c655 100644 --- a/app/cronjob/trailingTrade/step/__tests__/ensure-grid-trade-order-executed.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/ensure-grid-trade-order-executed.test.js @@ -5,7 +5,6 @@ describe('ensure-grid-trade-order-executed.js', () => { let result; let rawData; - let binanceMock; let slackMock; let loggerMock; let PubSubMock; @@ -18,11 +17,8 @@ describe('ensure-grid-trade-order-executed.js', () => { let mockSaveSymbolGridTrade; - let mockGetGridTradeOrder; + let mockGetGridTradeLastOrder; let mockDeleteGridTradeOrder; - let mockSaveGridTradeOrder; - - const momentDateTime = '2020-01-02T00:00:00+00:00'; describe('execute', () => { beforeEach(async () => { @@ -35,9 +31,8 @@ describe('ensure-grid-trade-order-executed.js', () => { jest.requireActual('moment')(nextCheck || '2020-01-02T00:00:00+00:00') ); - const { binance, slack, logger, PubSub } = require('../../../../helpers'); + const { slack, logger, PubSub } = require('../../../../helpers'); - binanceMock = binance; slackMock = slack; loggerMock = logger; PubSubMock = PubSub; @@ -45,7 +40,6 @@ describe('ensure-grid-trade-order-executed.js', () => { PubSubMock.publish = jest.fn().mockResolvedValue(true); slackMock.sendMessage = jest.fn().mockResolvedValue(true); - binanceMock.client.getOrder = jest.fn().mockResolvedValue([]); mockCalculateLastBuyPrice = jest.fn().mockResolvedValue(true); mockGetAPILimit = jest.fn().mockResolvedValue(10); @@ -55,9 +49,8 @@ describe('ensure-grid-trade-order-executed.js', () => { mockSaveSymbolGridTrade = jest.fn().mockResolvedValue(true); - mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); + mockGetGridTradeLastOrder = jest.fn().mockResolvedValue(null); mockDeleteGridTradeOrder = jest.fn().mockResolvedValue(true); - mockSaveGridTradeOrder = jest.fn().mockResolvedValue(true); }); describe('when api limit is exceed', () => { @@ -77,9 +70,8 @@ describe('ensure-grid-trade-order-executed.js', () => { })); jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder, - deleteGridTradeOrder: mockDeleteGridTradeOrder, - saveGridTradeOrder: mockSaveGridTradeOrder + getGridTradeLastOrder: mockGetGridTradeLastOrder, + deleteGridTradeOrder: mockDeleteGridTradeOrder })); const step = require('../ensure-grid-trade-order-executed'); @@ -140,22 +132,14 @@ describe('ensure-grid-trade-order-executed.js', () => { result = await step.execute(loggerMock, rawData); }); - it('does not trigger getGridTradeOrder', () => { - expect(mockGetGridTradeOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger saveGridTradeOrder', () => { - expect(mockSaveGridTradeOrder).not.toHaveBeenCalled(); + it('does not trigger getGridTradeLastOrder', () => { + expect(mockGetGridTradeLastOrder).not.toHaveBeenCalled(); }); it('does not trigger deleteGridTradeOrder', () => { expect(mockDeleteGridTradeOrder).not.toHaveBeenCalled(); }); - it('does not trigger binance.client.getOrder', () => { - expect(binanceMock.client.getOrder).not.toHaveBeenCalled(); - }); - it('does not trigger disableAction', () => { expect(mockDisableAction).not.toHaveBeenCalled(); }); @@ -164,7 +148,11 @@ describe('ensure-grid-trade-order-executed.js', () => { expect(mockSaveOrderStats).not.toHaveBeenCalled(); }); - it('returns epxected result', () => { + it('does not trigger slack.sendMessage', () => { + expect(slackMock.sendMessage).not.toHaveBeenCalled(); + }); + + it('returns expected result', () => { expect(result).toStrictEqual(rawData); }); }); @@ -175,13 +163,13 @@ describe('ensure-grid-trade-order-executed.js', () => { desc: 'last buy order is empty', symbol: 'BNBUSDT', lastBuyOrder: null, - getOrder: null, saveSymbolGridTrade: null }, { desc: 'last buy order is FILLED - currentGridTradeIndex: 0', symbol: 'BNBUSDT', notifyDebug: true, + notifyOrderExecute: true, lastBuyOrder: { symbol: 'BNBUSDT', side: 'BUY', @@ -191,17 +179,14 @@ describe('ensure-grid-trade-order-executed.js', () => { price: '302.09000000', origQty: '0.03320000', stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' + currentGridTradeIndex: 0 }, - getOrder: null, saveSymbolGridTrade: { buy: [ { executed: true, executedOrder: { currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00', orderId: 2705449295, origQty: '0.03320000', price: '302.09000000', @@ -249,6 +234,7 @@ describe('ensure-grid-trade-order-executed.js', () => { desc: 'last buy order is FILLED - currentGridTradeIndex: 1', symbol: 'BNBUSDT', notifyDebug: false, + notifyOrderExecute: false, lastBuyOrder: { symbol: 'BNBUSDT', side: 'BUY', @@ -258,10 +244,8 @@ describe('ensure-grid-trade-order-executed.js', () => { price: '302.09000000', origQty: '0.03320000', stopPrice: '301.80000000', - currentGridTradeIndex: 1, - nextCheck: '2020-01-01T23:59:00+00:00' + currentGridTradeIndex: 1 }, - getOrder: null, saveSymbolGridTrade: { buy: [ { @@ -276,7 +260,6 @@ describe('ensure-grid-trade-order-executed.js', () => { executed: true, executedOrder: { currentGridTradeIndex: 1, - nextCheck: '2020-01-01T23:59:00+00:00', orderId: 2705449295, origQty: '0.03320000', price: '302.09000000', @@ -313,25 +296,7 @@ describe('ensure-grid-trade-order-executed.js', () => { } }, { - desc: 'last buy order is NEW and still NEW before checking the order', - symbol: 'BNBUSDT', - lastBuyOrder: { - symbol: 'BNBUSDT', - side: 'BUY', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-02T00:01:00+00:00' - }, - getOrder: null, - saveSymbolGridTrade: null - }, - { - desc: 'last buy order is NEW and still NEW after checking the order', + desc: 'last buy order is NEW', symbol: 'BNBUSDT', lastBuyOrder: { symbol: 'BNBUSDT', @@ -342,38 +307,15 @@ describe('ensure-grid-trade-order-executed.js', () => { price: '302.09000000', origQty: '0.03320000', stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }, - getOrder: { - symbol: 'BNBUSDT', - side: 'BUY', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000' + currentGridTradeIndex: 0 }, saveSymbolGridTrade: null }, ...['CANCELED', 'REJECTED', 'EXPIRED', 'PENDING_CANCEL'].map( status => ({ - desc: `last buy order is NEW and become ${status}`, + desc: `last buy order is ${status}`, symbol: 'BNBUSDT', lastBuyOrder: { - symbol: 'BNBUSDT', - side: 'BUY', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }, - getOrder: { symbol: 'BNBUSDT', side: 'BUY', status, @@ -381,194 +323,15 @@ describe('ensure-grid-trade-order-executed.js', () => { orderId: 2705449295, price: '302.09000000', origQty: '0.03320000', - stopPrice: '301.80000000' + stopPrice: '301.80000000', + currentGridTradeIndex: 0 }, saveSymbolGridTrade: null }) - ), - { - desc: 'last buy order is NEW and now FILLED', - symbol: 'BNBUSDT', - notifyDebug: false, - lastBuyOrder: { - symbol: 'BNBUSDT', - side: 'BUY', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }, - getOrder: { - symbol: 'BNBUSDT', - side: 'BUY', - status: 'FILLED', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000' - }, - saveSymbolGridTrade: { - buy: [ - { - executed: true, - executedOrder: { - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00', - orderId: 2705449295, - origQty: '0.03320000', - price: '302.09000000', - side: 'BUY', - status: 'FILLED', - stopPrice: '301.80000000', - symbol: 'BNBUSDT', - type: 'STOP_LOSS_LIMIT' - }, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - stopPercentage: 1.025, - triggerPercentage: 1 - }, - { - executed: false, - executedOrder: null, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - stopPercentage: 1.025, - triggerPercentage: 0.8 - } - ], - sell: [ - { - executed: false, - executedOrder: null, - limitPercentage: 0.984, - quantityPercentage: 0.8, - stopPercentage: 0.985, - triggerPercentage: 1.03 - }, - { - executed: false, - executedOrder: null, - limitPercentage: 0.974, - quantityPercentage: 1, - stopPercentage: 0.975, - triggerPercentage: 1.05 - } - ] - } - }, - { - desc: 'last buy order is NEW and now FILLED - currentGridTradeIndex: 1', - symbol: 'BNBUSDT', - notifyDebug: true, - lastBuyOrder: { - symbol: 'BNBUSDT', - side: 'BUY', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000', - currentGridTradeIndex: 1, - nextCheck: '2020-01-01T23:59:00+00:00' - }, - getOrder: { - symbol: 'BNBUSDT', - side: 'BUY', - status: 'FILLED', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000' - }, - saveSymbolGridTrade: { - buy: [ - { - executed: false, - executedOrder: null, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - stopPercentage: 1.025, - triggerPercentage: 1 - }, - { - executed: true, - executedOrder: { - currentGridTradeIndex: 1, - nextCheck: '2020-01-01T23:59:00+00:00', - orderId: 2705449295, - origQty: '0.03320000', - price: '302.09000000', - side: 'BUY', - status: 'FILLED', - stopPrice: '301.80000000', - symbol: 'BNBUSDT', - type: 'STOP_LOSS_LIMIT' - }, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - stopPercentage: 1.025, - triggerPercentage: 0.8 - } - ], - sell: [ - { - executed: false, - executedOrder: null, - limitPercentage: 0.984, - quantityPercentage: 0.8, - stopPercentage: 0.985, - triggerPercentage: 1.03 - }, - { - executed: false, - executedOrder: null, - limitPercentage: 0.974, - quantityPercentage: 1, - stopPercentage: 0.975, - triggerPercentage: 1.05 - } - ] - } - }, - { - desc: 'last buy order is NEW but error', - symbol: 'BNBUSDT', - lastBuyOrder: { - symbol: 'BNBUSDT', - side: 'BUY', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }, - getOrder: 'error', - saveSymbolGridTrade: null - } - ].forEach((t, index) => { + ) + ].forEach(t => { describe(`${t.desc}`, () => { beforeEach(async () => { - if (t.getOrder === 'error') { - binanceMock.client.getOrder = jest - .fn() - .mockRejectedValue(new Error('something happened')); - } else { - binanceMock.client.getOrder = jest - .fn() - .mockResolvedValue(t.getOrder); - } - jest.mock('../../../trailingTradeHelper/common', () => ({ calculateLastBuyPrice: mockCalculateLastBuyPrice, getAPILimit: mockGetAPILimit, @@ -581,10 +344,13 @@ describe('ensure-grid-trade-order-executed.js', () => { saveSymbolGridTrade: mockSaveSymbolGridTrade })); - mockGetGridTradeOrder = jest + mockGetGridTradeLastOrder = jest .fn() - .mockImplementation((_logger, key) => { - if (key === `${t.symbol}-grid-trade-last-buy-order`) { + .mockImplementation((_logger, symbol, side) => { + if ( + `${t.symbol}-grid-trade-last-buy-order` === + `${symbol}-grid-trade-last-${side}-order` + ) { return t.lastBuyOrder; } @@ -592,9 +358,8 @@ describe('ensure-grid-trade-order-executed.js', () => { }); jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder, - deleteGridTradeOrder: mockDeleteGridTradeOrder, - saveGridTradeOrder: mockSaveGridTradeOrder + getGridTradeLastOrder: mockGetGridTradeLastOrder, + deleteGridTradeOrder: mockDeleteGridTradeOrder })); const step = require('../ensure-grid-trade-order-executed'); @@ -603,7 +368,7 @@ describe('ensure-grid-trade-order-executed.js', () => { symbol: t.symbol, action: 'not-determined', featureToggle: { - notifyOrderExecute: index % 2, + notifyOrderExecute: t.notifyOrderExecute || false, notifyDebug: t.notifyDebug || false }, symbolConfiguration: { @@ -659,22 +424,15 @@ describe('ensure-grid-trade-order-executed.js', () => { }); it('triggers getGridTradeOrder for getting cached order', () => { - expect(mockGetGridTradeOrder).toHaveBeenCalledWith( + expect(mockGetGridTradeLastOrder).toHaveBeenCalledWith( loggerMock, - `${t.symbol}-grid-trade-last-buy-order` + t.symbol, + 'buy' ); }); if (t.lastBuyOrder === null) { // If last order is not found - it('does not trigger binance.client.getOrder as order not found', () => { - expect(binanceMock.client.getOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger saveGridTradeOrder as order not found', () => { - expect(mockSaveGridTradeOrder).not.toHaveBeenCalled(); - }); - it('does not trigger deleteGridTradeOrder as order not found', () => { expect(mockDeleteGridTradeOrder).not.toHaveBeenCalled(); }); @@ -736,178 +494,59 @@ describe('ensure-grid-trade-order-executed.js', () => { 'BNBUSDT' ]); }); - } else { - if (t.getOrder === 'error') { - // order throws an error - it('triggers saveGridTradeOrder for last buy order as order throws error', () => { - expect(mockSaveGridTradeOrder).toHaveBeenCalledWith( - loggerMock, - `${t.symbol}-grid-trade-last-buy-order`, - { - ...t.lastBuyOrder, - // 10 secs - nextCheck: '2020-01-02T00:00:10+00:00' - } - ); - }); - - it('does not trigger saveSymbolGridTrade as order throws error', () => { - expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); - }); - - it('does not trigger deleteGridTradeOrder for last buy order as order throws error', () => { - expect(mockDeleteGridTradeOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger disableAction as order throws error', () => { - expect(mockDisableAction).not.toHaveBeenCalled(); - }); - it('does not trigger saveOrderStats as order throws error', () => { - expect(mockSaveOrderStats).not.toHaveBeenCalled(); - }); - } else if ( - Date.parse(t.lastBuyOrder.nextCheck) < Date.parse(momentDateTime) - ) { - // time to check order - it('triggers binance.client.getOrder as time to check', () => { - expect(binanceMock.client.getOrder).toHaveBeenCalledWith({ - symbol: t.symbol, - orderId: t.lastBuyOrder.orderId - }); + if (t.notifyOrderExecute === true) { + it('triggers slack.sendMessage due to filled order', () => { + expect(slackMock.sendMessage).toHaveBeenCalledWith( + expect.stringContaining('Order Filled') + ); }); - - if (t.getOrder.status === 'FILLED') { - // do filled thing - it('triggers calculated last buy price as order filled after getting order result', () => { - expect(mockCalculateLastBuyPrice).toHaveBeenCalledWith( - loggerMock, - t.symbol, - t.getOrder - ); - }); - - it('triggers save symbol grid trade as order filled after getting order result', () => { - expect(mockSaveSymbolGridTrade).toHaveBeenCalledWith( - loggerMock, - t.symbol, - t.saveSymbolGridTrade - ); - }); - - it('triggers deleteGridTradeOrder as order filled after getting order result', () => { - expect(mockDeleteGridTradeOrder).toHaveBeenCalledWith( - loggerMock, - `${t.symbol}-grid-trade-last-buy-order` - ); - }); - - it('triggers disableAction after getting order result', () => { - expect(mockDisableAction).toHaveBeenCalledWith( - loggerMock, - t.symbol, - { - disabledBy: 'buy filled order', - message: - 'Disabled action after confirming filled grid trade order.', - canResume: false, - canRemoveLastBuyPrice: false - }, - 20 - ); - }); - - it('triggers saveOrderStats after getting order result', () => { - expect(mockSaveOrderStats).toHaveBeenCalledWith(loggerMock, [ - 'BTCUSDT', - 'BNBUSDT' - ]); - }); - } else if ( - ['CANCELED', 'REJECTED', 'EXPIRED', 'PENDING_CANCEL'].includes( - t.getOrder.status - ) === true - ) { - // do cancel thing - it('triggers deleteGridTradeOrder due to cancelled order', () => { - expect(mockDeleteGridTradeOrder).toHaveBeenCalledWith( - loggerMock, - `${t.symbol}-grid-trade-last-buy-order` - ); - }); - - it('does not trigger saveSymbolGridTrade due to cancelled order', () => { - expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); - }); - - it('does not trigger disableAction due to cancelled order', () => { - expect(mockDisableAction).not.toHaveBeenCalled(); - }); - - it('triggers saveOrderStats due to cancelled order', () => { - expect(mockSaveOrderStats).toHaveBeenCalledWith(loggerMock, [ - 'BTCUSDT', - 'BNBUSDT' - ]); - }); - } else { - // do else thing - it('triggers saveGridTradeOrder for last buy order as not filled', () => { - expect(mockSaveGridTradeOrder).toHaveBeenCalledWith( - loggerMock, - `${t.symbol}-grid-trade-last-buy-order`, - { - ...t.getOrder, - currentGridTradeIndex: - t.lastBuyOrder.currentGridTradeIndex, - // 10 secs - nextCheck: '2020-01-02T00:00:10+00:00' - } - ); - }); - - it('does not trigger deleteGridTradeOrder for last buy order as not filled', () => { - expect(mockDeleteGridTradeOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger saveSymbolGridTrade as not filled', () => { - expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); - }); - - it('does not trigger disableAction as not filled', () => { - expect(mockDisableAction).not.toHaveBeenCalled(); - }); - - it('does not trigger saveOrderStats as not filled', () => { - expect(mockSaveOrderStats).not.toHaveBeenCalled(); - }); - } - } else if ( - Date.parse(t.lastBuyOrder.nextCheck) > Date.parse(momentDateTime) - ) { - // no need to check - it('does not trigger binance.client.getOrder because time is not yet to check', () => { - expect(binanceMock.client.getOrder).not.toHaveBeenCalled(); + } else { + it('does not trigger slack.sendMessage due to filled order', () => { + expect(slackMock.sendMessage).not.toHaveBeenCalledWith( + expect.stringContaining('Order Filled') + ); }); + } + } else if ( + ['CANCELED', 'REJECTED', 'EXPIRED', 'PENDING_CANCEL'].includes( + t.lastBuyOrder.status + ) === true + ) { + // do cancel thing + it('triggers deleteGridTradeOrder due to cancelled order', () => { + expect(mockDeleteGridTradeOrder).toHaveBeenCalledWith( + loggerMock, + `${t.symbol}-grid-trade-last-buy-order` + ); + }); - it('does not trigger saveGridTradeOrder because time is not yet to check', () => { - expect(mockSaveGridTradeOrder).not.toHaveBeenCalled(); - }); + it('does not trigger saveSymbolGridTrade due to cancelled order', () => { + expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); + }); - it('does not trigger deleteGridTradeOrder because time is not yet to check', () => { - expect(mockDeleteGridTradeOrder).not.toHaveBeenCalled(); - }); + it('does not trigger disableAction due to cancelled order', () => { + expect(mockDisableAction).not.toHaveBeenCalled(); + }); - it('does not trigger saveSymbolGridTrade because time is not yet to check', () => { - expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); - }); + it('triggers saveOrderStats due to cancelled order', () => { + expect(mockSaveOrderStats).toHaveBeenCalledWith(loggerMock, [ + 'BTCUSDT', + 'BNBUSDT' + ]); + }); - it('does not trigger disableAction because time is not yet to check', () => { - expect(mockDisableAction).not.toHaveBeenCalled(); + if (t.notifyOrderExecute === true) { + it('triggers slack.sendMessage due to cancelled order', () => { + expect(slackMock.sendMessage).toHaveBeenCalledWith( + expect.stringContaining('Order Removed') + ); }); - - it('does not trigger saveOrderStats because time is not yet to check', () => { - expect(mockSaveOrderStats).not.toHaveBeenCalled(); + } else { + it('does not trigger slack.sendMessage due to cancelled order', () => { + expect(slackMock.sendMessage).not.toHaveBeenCalledWith( + expect.stringContaining('Order Removed') + ); }); } } @@ -941,10 +580,8 @@ describe('ensure-grid-trade-order-executed.js', () => { price: '302.09000000', origQty: '0.03320000', stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' + currentGridTradeIndex: 0 }, - getOrder: null, saveSymbolGridTrade: { buy: [ { @@ -969,7 +606,6 @@ describe('ensure-grid-trade-order-executed.js', () => { executed: true, executedOrder: { currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00', orderId: 2705449295, origQty: '0.03320000', price: '302.09000000', @@ -1008,10 +644,8 @@ describe('ensure-grid-trade-order-executed.js', () => { price: '302.09000000', origQty: '0.03320000', stopPrice: '301.80000000', - currentGridTradeIndex: 1, - nextCheck: '2020-01-01T23:59:00+00:00' + currentGridTradeIndex: 1 }, - getOrder: null, saveSymbolGridTrade: { buy: [ { @@ -1044,7 +678,6 @@ describe('ensure-grid-trade-order-executed.js', () => { executed: true, executedOrder: { currentGridTradeIndex: 1, - nextCheck: '2020-01-01T23:59:00+00:00', orderId: 2705449295, origQty: '0.03320000', price: '302.09000000', @@ -1063,7 +696,7 @@ describe('ensure-grid-trade-order-executed.js', () => { } }, { - desc: 'last sell order is NEW and still NEW before checking the order', + desc: 'last sell order is NEW', symbol: 'BNBUSDT', lastSellOrder: { symbol: 'BNBUSDT', @@ -1074,56 +707,15 @@ describe('ensure-grid-trade-order-executed.js', () => { price: '302.09000000', origQty: '0.03320000', stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-02T00:01:00+00:00' + currentGridTradeIndex: 0 }, - getOrder: null, saveSymbolGridTrade: null }, - { - desc: 'last sell order is NEW and still NEW after checking the order', - symbol: 'BNBUSDT', - lastSellOrder: { - symbol: 'BNBUSDT', - side: 'SELL', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }, - getOrder: { - symbol: 'BNBUSDT', - side: 'SELL', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000' - }, - saveSymbolGridTrade: null - }, - ...['CANCELED', 'REJECTED', 'EXPIRED', 'PENDING_CANCEL'].map( - status => ({ - desc: `last sell order is NEW and become ${status}`, + ...['CANCELED', 'REJECTED', 'EXPIRED', 'PENDING_CANCEL'].map( + status => ({ + desc: `last sell order is ${status}`, symbol: 'BNBUSDT', lastSellOrder: { - symbol: 'BNBUSDT', - side: 'SELL', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }, - getOrder: { symbol: 'BNBUSDT', side: 'SELL', status, @@ -1131,193 +723,15 @@ describe('ensure-grid-trade-order-executed.js', () => { orderId: 2705449295, price: '302.09000000', origQty: '0.03320000', - stopPrice: '301.80000000' + stopPrice: '301.80000000', + currentGridTradeIndex: 0 }, saveSymbolGridTrade: null }) - ), - { - desc: 'last sell order is NEW and now FILLED - currentGridTradeIndex: 0', - symbol: 'BNBUSDT', - notifyDebug: true, - lastSellOrder: { - symbol: 'BNBUSDT', - side: 'SELL', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }, - getOrder: { - symbol: 'BNBUSDT', - side: 'SELL', - status: 'FILLED', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000' - }, - saveSymbolGridTrade: { - buy: [ - { - executed: false, - executedOrder: null, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - stopPercentage: 1.025, - triggerPercentage: 1 - }, - { - executed: false, - executedOrder: null, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - stopPercentage: 1.025, - triggerPercentage: 0.8 - } - ], - sell: [ - { - executed: true, - executedOrder: { - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00', - orderId: 2705449295, - origQty: '0.03320000', - price: '302.09000000', - side: 'SELL', - status: 'FILLED', - stopPrice: '301.80000000', - symbol: 'BNBUSDT', - type: 'STOP_LOSS_LIMIT' - }, - limitPercentage: 0.984, - quantityPercentage: 0.8, - stopPercentage: 0.985, - triggerPercentage: 1.03 - }, - { - executed: false, - executedOrder: null, - limitPercentage: 0.974, - quantityPercentage: 1, - stopPercentage: 0.975, - triggerPercentage: 1.05 - } - ] - } - }, - { - desc: 'last sell order is NEW and now FILLED - currentGridTradeIndex: 1', - symbol: 'BNBUSDT', - notifyDebug: false, - lastSellOrder: { - symbol: 'BNBUSDT', - side: 'SELL', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000', - currentGridTradeIndex: 1, - nextCheck: '2020-01-01T23:59:00+00:00' - }, - getOrder: { - symbol: 'BNBUSDT', - side: 'SELL', - status: 'FILLED', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000' - }, - saveSymbolGridTrade: { - buy: [ - { - executed: false, - executedOrder: null, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - stopPercentage: 1.025, - triggerPercentage: 1 - }, - { - executed: false, - executedOrder: null, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - stopPercentage: 1.025, - triggerPercentage: 0.8 - } - ], - sell: [ - { - executed: false, - executedOrder: null, - limitPercentage: 0.984, - quantityPercentage: 0.8, - stopPercentage: 0.985, - triggerPercentage: 1.03 - }, - { - executed: true, - executedOrder: { - currentGridTradeIndex: 1, - nextCheck: '2020-01-01T23:59:00+00:00', - orderId: 2705449295, - origQty: '0.03320000', - price: '302.09000000', - side: 'SELL', - status: 'FILLED', - stopPrice: '301.80000000', - symbol: 'BNBUSDT', - type: 'STOP_LOSS_LIMIT' - }, - limitPercentage: 0.974, - quantityPercentage: 1, - stopPercentage: 0.975, - triggerPercentage: 1.05 - } - ] - } - }, - { - desc: 'last sell order is NEW but error', - symbol: 'BNBUSDT', - lastSellOrder: { - symbol: 'BNBUSDT', - side: 'SELL', - status: 'NEW', - type: 'STOP_LOSS_LIMIT', - orderId: 2705449295, - price: '302.09000000', - origQty: '0.03320000', - stopPrice: '301.80000000', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }, - getOrder: 'error', - saveSymbolGridTrade: {} - } + ) ].forEach((t, index) => { describe(`${t.desc}`, () => { beforeEach(async () => { - if (t.getOrder === 'error') { - binanceMock.client.getOrder = jest - .fn() - .mockRejectedValue(new Error('something happened')); - } else { - binanceMock.client.getOrder = jest - .fn() - .mockResolvedValue(t.getOrder); - } jest.mock('../../../trailingTradeHelper/common', () => ({ calculateLastBuyPrice: mockCalculateLastBuyPrice, getAPILimit: mockGetAPILimit, @@ -1330,10 +744,13 @@ describe('ensure-grid-trade-order-executed.js', () => { saveSymbolGridTrade: mockSaveSymbolGridTrade })); - mockGetGridTradeOrder = jest + mockGetGridTradeLastOrder = jest .fn() - .mockImplementation((_logger, key) => { - if (key === `${t.symbol}-grid-trade-last-sell-order`) { + .mockImplementation((_logger, symbol, side) => { + if ( + `${t.symbol}-grid-trade-last-sell-order` === + `${symbol}-grid-trade-last-${side}-order` + ) { return t.lastSellOrder; } @@ -1341,9 +758,8 @@ describe('ensure-grid-trade-order-executed.js', () => { }); jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder, - deleteGridTradeOrder: mockDeleteGridTradeOrder, - saveGridTradeOrder: mockSaveGridTradeOrder + getGridTradeLastOrder: mockGetGridTradeLastOrder, + deleteGridTradeOrder: mockDeleteGridTradeOrder })); const step = require('../ensure-grid-trade-order-executed'); @@ -1352,7 +768,7 @@ describe('ensure-grid-trade-order-executed.js', () => { symbol: t.symbol, action: 'not-determined', featureToggle: { - notifyOrderExecute: index % 2, + notifyOrderExecute: true, notifyDebug: index % 2 }, symbolConfiguration: { @@ -1408,22 +824,15 @@ describe('ensure-grid-trade-order-executed.js', () => { }); it('triggers getGridTradeOrder for getting cached order', () => { - expect(mockGetGridTradeOrder).toHaveBeenCalledWith( + expect(mockGetGridTradeLastOrder).toHaveBeenCalledWith( loggerMock, - `${t.symbol}-grid-trade-last-sell-order` + t.symbol, + 'sell' ); }); if (t.lastSellOrder === null) { // If last order is not found - it('does not trigger binance.client.getOrder as order not found', () => { - expect(binanceMock.client.getOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger saveGridTradeOrder as order not found', () => { - expect(mockSaveGridTradeOrder).not.toHaveBeenCalled(); - }); - it('does not trigger deleteGridTradeOrder as order not found', () => { expect(mockDeleteGridTradeOrder).not.toHaveBeenCalled(); }); @@ -1474,555 +883,50 @@ describe('ensure-grid-trade-order-executed.js', () => { 'BNBUSDT' ]); }); - } else { - if (t.getOrder === 'error') { - // order throws an error - it('triggers saveGridTradeOrder for last sell order as order throws error', () => { - expect(mockSaveGridTradeOrder).toHaveBeenCalledWith( - loggerMock, - `${t.symbol}-grid-trade-last-sell-order`, - { - ...t.lastSellOrder, - // 10 secs - nextCheck: '2020-01-02T00:00:10+00:00' - } - ); - }); - - it('does not trigger saveSymbolGridTrade as order throws error', () => { - expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); - }); - - it('does not trigger deleteGridTradeOrder for last sell order as order throws error', () => { - expect(mockDeleteGridTradeOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger disableAction as order throws error', () => { - expect(mockDisableAction).not.toHaveBeenCalled(); - }); - - it('does not trigger saveOrderStats as order throws error', () => { - expect(mockSaveOrderStats).not.toHaveBeenCalled(); - }); - } else if ( - Date.parse(t.lastSellOrder.nextCheck) < Date.parse(momentDateTime) - ) { - // time to check order - it('triggers binance.client.getOrder as time to check', () => { - expect(binanceMock.client.getOrder).toHaveBeenCalledWith({ - symbol: t.symbol, - orderId: t.lastSellOrder.orderId - }); - }); - - if (t.getOrder.status === 'FILLED') { - // do filled thing - it('triggers save symbol grid trade as order filled after getting order result', () => { - expect(mockSaveSymbolGridTrade).toHaveBeenCalledWith( - loggerMock, - t.symbol, - t.saveSymbolGridTrade - ); - }); - - it('triggers deleteGridTradeOrder as order filled after getting order result', () => { - expect(mockDeleteGridTradeOrder).toHaveBeenCalledWith( - loggerMock, - `${t.symbol}-grid-trade-last-sell-order` - ); - }); - - it('triggers disableAction after getting order result', () => { - expect(mockDisableAction).toHaveBeenCalledWith( - loggerMock, - t.symbol, - { - disabledBy: 'sell filled order', - message: - 'Disabled action after confirming filled grid trade order.', - canResume: false, - canRemoveLastBuyPrice: true - }, - 20 - ); - }); - - it('triggers saveOrderStats after getting order result', () => { - expect(mockSaveOrderStats).toHaveBeenCalledWith(loggerMock, [ - 'BTCUSDT', - 'BNBUSDT' - ]); - }); - } else if ( - ['CANCELED', 'REJECTED', 'EXPIRED', 'PENDING_CANCEL'].includes( - t.getOrder.status - ) === true - ) { - // do cancel thing - it('triggers deleteGridTradeOrder due to cancelled order', () => { - expect(mockDeleteGridTradeOrder).toHaveBeenCalledWith( - loggerMock, - `${t.symbol}-grid-trade-last-sell-order` - ); - }); - - it('does not trigger saveSymbolGridTrade due to cancelled order', () => { - expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); - }); - - it('does not trigger disableAction due to cancelled order', () => { - expect(mockDisableAction).not.toHaveBeenCalled(); - }); - - it('triggers saveOrderStats due to cancelled order', () => { - expect(mockSaveOrderStats).toHaveBeenCalledWith(loggerMock, [ - 'BTCUSDT', - 'BNBUSDT' - ]); - }); - } else { - // do else thing - it('triggers saveGridTradeOrder for last sell order as not filled', () => { - expect(mockSaveGridTradeOrder).toHaveBeenCalledWith( - loggerMock, - `${t.symbol}-grid-trade-last-sell-order`, - { - ...t.getOrder, - currentGridTradeIndex: - t.lastSellOrder.currentGridTradeIndex, - // 10 secs - nextCheck: '2020-01-02T00:00:10+00:00' - } - ); - }); - it('does not trigger deleteGridTradeOrder for last sell order as not filled', () => { - expect(mockDeleteGridTradeOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger saveSymbolGridTrade as not filled', () => { - expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); - }); - - it('does not trigger disableAction as not filled', () => { - expect(mockDisableAction).not.toHaveBeenCalled(); - }); - - it('does not trigger saveOrderStats as not filled', () => { - expect(mockSaveOrderStats).not.toHaveBeenCalled(); - }); - } - } else if ( - Date.parse(t.lastSellOrder.nextCheck) > Date.parse(momentDateTime) - ) { - // no need to check - it('does not trigger binance.client.getOrder because time is not yet to check', () => { - expect(binanceMock.client.getOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger saveGridTradeOrder because time is not yet to check', () => { - expect(mockSaveGridTradeOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger deleteGridTradeOrder because time is not yet to check', () => { - expect(mockDeleteGridTradeOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger saveSymbolGridTrade because time is not yet to check', () => { - expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); - }); - - it('does not trigger disableAction because time is not yet to check', () => { - expect(mockDisableAction).not.toHaveBeenCalled(); - }); - - it('does not trigger saveOrderStats because time is not yet to check', () => { - expect(mockSaveOrderStats).not.toHaveBeenCalled(); - }); - } - } - - it('returns result', () => { - expect(result).toStrictEqual(rawData); - }); - }); - }); - }); - - describe('slackMessageOrderFilled', () => { - describe('when orderParams does not have type for some reason', () => { - beforeEach(async () => { - binanceMock.client.getOrder = jest.fn().mockResolvedValue({ - symbol: 'BNBUSDT', - side: 'BUY', - status: 'FILLED', - type: 'STOP_LOSS_LIMIT' - }); - - jest.mock('../../../trailingTradeHelper/common', () => ({ - calculateLastBuyPrice: mockCalculateLastBuyPrice, - getAPILimit: mockGetAPILimit, - isExceedAPILimit: mockIsExceedAPILimit, - disableAction: mockDisableAction, - saveOrderStats: mockSaveOrderStats - })); - - jest.mock('../../../trailingTradeHelper/configuration', () => ({ - saveSymbolGridTrade: mockSaveSymbolGridTrade - })); - - mockGetGridTradeOrder = jest - .fn() - .mockImplementation((_logger, key) => { - if (key === `BTCUSDT-grid-trade-last-buy-order`) { - return { - symbol: 'BTCUSDT', - side: 'BUY', - status: 'NEW', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }; - } - return null; + it('triggers slack.sendMessage due to filled order', () => { + expect(slackMock.sendMessage).toHaveBeenCalledWith( + expect.stringContaining('Order Filled') + ); }); - - jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder, - deleteGridTradeOrder: mockDeleteGridTradeOrder, - saveGridTradeOrder: mockSaveGridTradeOrder - })); - - const step = require('../ensure-grid-trade-order-executed'); - - rawData = { - symbol: 'BTCUSDT', - action: 'not-determined', - featureToggle: { - notifyOrderExecute: true, - notifyDebug: false - }, - symbolConfiguration: { - symbols: ['BTCUSDT', 'BNBUSDT'], - buy: { - gridTrade: [ - { - triggerPercentage: 1, - stopPercentage: 1.025, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - executed: false, - executedOrder: null - } - ] - }, - sell: { - gridTrade: [ - { - triggerPercentage: 1.03, - stopPercentage: 0.985, - limitPercentage: 0.984, - quantityPercentage: 1, - executed: false, - executedOrder: null - } - ] - }, - system: { - checkOrderExecutePeriod: 10, - temporaryDisableActionAfterConfirmingOrder: 20 - } - } - }; - - result = await step.execute(loggerMock, rawData); - }); - - it('triggers slack.sendMessage', () => { - expect(slackMock.sendMessage).toHaveBeenCalledWith( - expect.stringContaining('STOP_LOSS_LIMIT') - ); - }); - }); - - describe('when orderParams/orderResult is empty for some reason', () => { - beforeEach(async () => { - binanceMock.client.getOrder = jest.fn().mockResolvedValue({ - symbol: 'BNBUSDT', - side: 'BUY', - status: 'FILLED' - }); - - jest.mock('../../../trailingTradeHelper/common', () => ({ - calculateLastBuyPrice: mockCalculateLastBuyPrice, - getAPILimit: mockGetAPILimit, - isExceedAPILimit: mockIsExceedAPILimit, - disableAction: mockDisableAction, - saveOrderStats: mockSaveOrderStats - })); - - jest.mock('../../../trailingTradeHelper/configuration', () => ({ - saveSymbolGridTrade: mockSaveSymbolGridTrade - })); - - mockGetGridTradeOrder = jest - .fn() - .mockImplementation((_logger, key) => { - if (key === `BTCUSDT-grid-trade-last-buy-order`) { - return { - symbol: 'BTCUSDT', - side: 'BUY', - status: 'NEW', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }; - } - return null; + } else if ( + ['CANCELED', 'REJECTED', 'EXPIRED', 'PENDING_CANCEL'].includes( + t.lastSellOrder.status + ) === true + ) { + // do cancel thing + it('triggers deleteGridTradeOrder due to cancelled order', () => { + expect(mockDeleteGridTradeOrder).toHaveBeenCalledWith( + loggerMock, + `${t.symbol}-grid-trade-last-sell-order` + ); }); - jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder, - deleteGridTradeOrder: mockDeleteGridTradeOrder, - saveGridTradeOrder: mockSaveGridTradeOrder - })); - - const step = require('../ensure-grid-trade-order-executed'); - - rawData = { - symbol: 'BTCUSDT', - action: 'not-determined', - featureToggle: { - notifyOrderExecute: true, - notifyDebug: false - }, - symbolConfiguration: { - symbols: ['BTCUSDT', 'BNBUSDT'], - buy: { - gridTrade: [ - { - triggerPercentage: 1, - stopPercentage: 1.025, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - executed: false, - executedOrder: null - } - ] - }, - sell: { - gridTrade: [ - { - triggerPercentage: 1.03, - stopPercentage: 0.985, - limitPercentage: 0.984, - quantityPercentage: 1, - executed: false, - executedOrder: null - } - ] - }, - system: { - checkOrderExecutePeriod: 10, - temporaryDisableActionAfterConfirmingOrder: 20 - } - } - }; - - result = await step.execute(loggerMock, rawData); - }); - - it('triggers slack.sendMessage', () => { - expect(slackMock.sendMessage).toHaveBeenCalledWith( - expect.stringContaining('Undefined') - ); - }); - }); - }); - - describe('slackMessageOrderDeleted', () => { - describe('when orderParams does not have type for some reason', () => { - beforeEach(async () => { - binanceMock.client.getOrder = jest.fn().mockResolvedValue({ - symbol: 'BNBUSDT', - side: 'BUY', - status: 'CANCELED', - type: 'STOP_LOSS_LIMIT' - }); - - jest.mock('../../../trailingTradeHelper/common', () => ({ - calculateLastBuyPrice: mockCalculateLastBuyPrice, - getAPILimit: mockGetAPILimit, - isExceedAPILimit: mockIsExceedAPILimit, - disableAction: mockDisableAction, - saveOrderStats: mockSaveOrderStats - })); - - jest.mock('../../../trailingTradeHelper/configuration', () => ({ - saveSymbolGridTrade: mockSaveSymbolGridTrade - })); - - mockGetGridTradeOrder = jest - .fn() - .mockImplementation((_logger, key) => { - if (key === `BTCUSDT-grid-trade-last-buy-order`) { - return { - symbol: 'BTCUSDT', - side: 'BUY', - status: 'NEW', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }; - } - return null; + it('does not trigger saveSymbolGridTrade due to cancelled order', () => { + expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); }); - jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder, - deleteGridTradeOrder: mockDeleteGridTradeOrder, - saveGridTradeOrder: mockSaveGridTradeOrder - })); - - const step = require('../ensure-grid-trade-order-executed'); - - rawData = { - symbol: 'BTCUSDT', - action: 'not-determined', - featureToggle: { - notifyOrderExecute: true, - notifyDebug: false - }, - symbolConfiguration: { - symbols: ['BTCUSDT', 'BNBUSDT'], - buy: { - gridTrade: [ - { - triggerPercentage: 1, - stopPercentage: 1.025, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - executed: false, - executedOrder: null - } - ] - }, - sell: { - gridTrade: [ - { - triggerPercentage: 1.03, - stopPercentage: 0.985, - limitPercentage: 0.984, - quantityPercentage: 1, - executed: false, - executedOrder: null - } - ] - }, - system: { - checkOrderExecutePeriod: 10, - temporaryDisableActionAfterConfirmingOrder: 20 - } - } - }; - - result = await step.execute(loggerMock, rawData); - }); - - it('triggers slack.sendMessage', () => { - expect(slackMock.sendMessage).toHaveBeenCalledWith( - expect.stringContaining('STOP_LOSS_LIMIT') - ); - }); - }); - - describe('when orderParams/orderResult is empty for some reason', () => { - beforeEach(async () => { - binanceMock.client.getOrder = jest.fn().mockResolvedValue({ - symbol: 'BNBUSDT', - side: 'BUY', - status: 'CANCELED' - }); - - jest.mock('../../../trailingTradeHelper/common', () => ({ - calculateLastBuyPrice: mockCalculateLastBuyPrice, - getAPILimit: mockGetAPILimit, - isExceedAPILimit: mockIsExceedAPILimit, - disableAction: mockDisableAction, - saveOrderStats: mockSaveOrderStats - })); - - jest.mock('../../../trailingTradeHelper/configuration', () => ({ - saveSymbolGridTrade: mockSaveSymbolGridTrade - })); - - mockGetGridTradeOrder = jest - .fn() - .mockImplementation((_logger, key) => { - if (key === `BTCUSDT-grid-trade-last-buy-order`) { - return { - symbol: 'BTCUSDT', - side: 'BUY', - status: 'NEW', - currentGridTradeIndex: 0, - nextCheck: '2020-01-01T23:59:00+00:00' - }; - } - return null; + it('does not trigger disableAction due to cancelled order', () => { + expect(mockDisableAction).not.toHaveBeenCalled(); }); - jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder, - deleteGridTradeOrder: mockDeleteGridTradeOrder, - saveGridTradeOrder: mockSaveGridTradeOrder - })); - - const step = require('../ensure-grid-trade-order-executed'); - - rawData = { - symbol: 'BTCUSDT', - action: 'not-determined', - featureToggle: { - notifyOrderExecute: true, - notifyDebug: false - }, - symbolConfiguration: { - symbols: ['BTCUSDT', 'BNBUSDT'], - buy: { - gridTrade: [ - { - triggerPercentage: 1, - stopPercentage: 1.025, - limitPercentage: 1.026, - maxPurchaseAmount: 10, - executed: false, - executedOrder: null - } - ] - }, - sell: { - gridTrade: [ - { - triggerPercentage: 1.03, - stopPercentage: 0.985, - limitPercentage: 0.984, - quantityPercentage: 1, - executed: false, - executedOrder: null - } - ] - }, - system: { - checkOrderExecutePeriod: 10, - temporaryDisableActionAfterConfirmingOrder: 20 - } - } - }; + it('triggers saveOrderStats due to cancelled order', () => { + expect(mockSaveOrderStats).toHaveBeenCalledWith(loggerMock, [ + 'BTCUSDT', + 'BNBUSDT' + ]); + }); - result = await step.execute(loggerMock, rawData); - }); + it('triggers slack.sendMessage due to cancelled order', () => { + expect(slackMock.sendMessage).toHaveBeenCalledWith( + expect.stringContaining('Order Removed') + ); + }); + } - it('triggers slack.sendMessage', () => { - expect(slackMock.sendMessage).toHaveBeenCalledWith( - expect.stringContaining('Undefined') - ); + it('returns result', () => { + expect(result).toStrictEqual(rawData); + }); }); }); }); diff --git a/app/cronjob/trailingTrade/step/__tests__/ensure-manual-order.test.js b/app/cronjob/trailingTrade/step/__tests__/ensure-manual-order.test.js index 738496b6..1a4e5515 100644 --- a/app/cronjob/trailingTrade/step/__tests__/ensure-manual-order.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/ensure-manual-order.test.js @@ -1,11 +1,8 @@ /* eslint-disable global-require */ -const moment = require('moment'); - describe('ensure-manual-order.js', () => { let result; let rawData; - let binanceMock; let slackMock; let loggerMock; let PubSubMock; @@ -19,7 +16,6 @@ describe('ensure-manual-order.js', () => { let mockGetManualOrders; let mockDeleteManualOrder; - let mockSaveManualOrder; describe('execute', () => { beforeEach(() => { @@ -27,9 +23,8 @@ describe('ensure-manual-order.js', () => { }); beforeEach(async () => { - const { binance, slack, logger, PubSub } = require('../../../../helpers'); + const { slack, logger, PubSub } = require('../../../../helpers'); - binanceMock = binance; slackMock = slack; loggerMock = logger; PubSubMock = PubSub; @@ -37,7 +32,6 @@ describe('ensure-manual-order.js', () => { PubSubMock.publish = jest.fn().mockResolvedValue(true); slackMock.sendMessage = jest.fn().mockResolvedValue(true); - binanceMock.client.getOrder = jest.fn().mockResolvedValue([]); mockCalculateLastBuyPrice = jest.fn().mockResolvedValue(true); mockGetAPILimit = jest.fn().mockResolvedValue(10); @@ -61,7 +55,6 @@ describe('ensure-manual-order.js', () => { mockGetManualOrders = jest.fn().mockResolvedValue(null); mockDeleteManualOrder = jest.fn().mockResolvedValue(true); - mockSaveManualOrder = jest.fn().mockResolvedValue(true); }); describe('when api limit exceeded', () => { @@ -81,8 +74,7 @@ describe('ensure-manual-order.js', () => { jest.mock('../../../trailingTradeHelper/order', () => ({ getManualOrders: mockGetManualOrders, - deleteManualOrder: mockDeleteManualOrder, - saveManualOrder: mockSaveManualOrder + deleteManualOrder: mockDeleteManualOrder })); const step = require('../ensure-manual-order'); @@ -101,10 +93,6 @@ describe('ensure-manual-order.js', () => { result = await step.execute(loggerMock, rawData); }); - it('does not trigger binance.client.getOrder', () => { - expect(binanceMock.client.getOrder).not.toHaveBeenCalled(); - }); - it('does not trigger deleteManualOrder', () => { expect(mockDeleteManualOrder).not.toHaveBeenCalled(); }); @@ -113,10 +101,6 @@ describe('ensure-manual-order.js', () => { expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); }); - it('does not trigger saveManualOrder', () => { - expect(mockSaveManualOrder).not.toHaveBeenCalled(); - }); - it('does not trigger calculateLastBuyPrice', () => { expect(mockCalculateLastBuyPrice).not.toHaveBeenCalled(); }); @@ -144,8 +128,7 @@ describe('ensure-manual-order.js', () => { jest.mock('../../../trailingTradeHelper/order', () => ({ getManualOrders: mockGetManualOrders, - deleteManualOrder: mockDeleteManualOrder, - saveManualOrder: mockSaveManualOrder + deleteManualOrder: mockDeleteManualOrder })); const step = require('../ensure-manual-order'); @@ -164,10 +147,6 @@ describe('ensure-manual-order.js', () => { result = await step.execute(loggerMock, rawData); }); - it('does not trigger binance.client.getOrder', () => { - expect(binanceMock.client.getOrder).not.toHaveBeenCalled(); - }); - it('does not trigger deleteManualOrder', () => { expect(mockDeleteManualOrder).not.toHaveBeenCalled(); }); @@ -176,10 +155,6 @@ describe('ensure-manual-order.js', () => { expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); }); - it('does not trigger saveManualOrder', () => { - expect(mockSaveManualOrder).not.toHaveBeenCalled(); - }); - it('does not trigger calculateLastBuyPrice', () => { expect(mockCalculateLastBuyPrice).not.toHaveBeenCalled(); }); @@ -198,7 +173,7 @@ describe('ensure-manual-order.js', () => { }); }); - describe('when manual buy order is already filled', () => { + describe('when manual order is already filled', () => { [ { desc: 'with LIMIT order and has existing last buy price', @@ -334,8 +309,7 @@ describe('ensure-manual-order.js', () => { jest.mock('../../../trailingTradeHelper/order', () => ({ getManualOrders: mockGetManualOrders, - deleteManualOrder: mockDeleteManualOrder, - saveManualOrder: mockSaveManualOrder + deleteManualOrder: mockDeleteManualOrder })); const step = require('../ensure-manual-order'); @@ -403,295 +377,6 @@ describe('ensure-manual-order.js', () => { }); describe('when manual order is not filled', () => { - [ - { - desc: 'with LIMIT order and FILLED', - symbol: 'CAKEUSDT', - lastBuyPriceDoc: { - lastBuyPrice: 30, - quantity: 3 - }, - featureToggle: { notifyDebug: false }, - orderId: 159653829, - cacheResults: [ - { - order: { - symbol: 'CAKEUSDT', - orderId: 159653829, - origQty: '1.00000000', - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'NEW', - type: 'LIMIT', - side: 'BUY', - nextCheck: moment() - .subtract(1, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') - } - } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'FILLED', - type: 'LIMIT', - side: 'BUY' - }, - expectedCalculateLastBuyPrice: true, - expectedFilledOrder: true - }, - { - desc: 'with MARKET order and FILLED', - symbol: 'CAKEUSDT', - lastBuyPriceDoc: { - lastBuyPrice: 30, - quantity: 3 - }, - featureToggle: { notifyDebug: true }, - orderId: 159653829, - cacheResults: [ - { - order: { - symbol: 'CAKEUSDT', - orderId: 159653829, - origQty: '1.00000000', - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'NEW', - type: 'MARKET', - side: 'BUY', - nextCheck: moment() - .subtract(5, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') - } - } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'FILLED', - type: 'MARKET', - side: 'BUY' - }, - expectedCalculateLastBuyPrice: true, - expectedFilledOrder: true - }, - { - desc: 'Sell with MARKET order and FILLED', - symbol: 'CAKEUSDT', - lastBuyPriceDoc: { - lastBuyPrice: 30, - quantity: 3 - }, - featureToggle: { notifyDebug: true }, - orderId: 159653829, - cacheResults: [ - { - order: { - symbol: 'CAKEUSDT', - orderId: 159653829, - origQty: '1.00000000', - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'NEW', - type: 'MARKET', - side: 'SELL', - nextCheck: moment() - .subtract(5, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') - } - } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'FILLED', - type: 'MARKET', - side: 'SELL' - }, - expectedCalculateLastBuyPrice: false, - expectedFilledOrder: true - }, - { - desc: 'with MARKET order and FILLED, but not yet to check', - symbol: 'CAKEUSDT', - lastBuyPriceDoc: { - lastBuyPrice: 30, - quantity: 3 - }, - featureToggle: { notifyDebug: false }, - orderId: 159653829, - cacheResults: [ - { - order: { - symbol: 'CAKEUSDT', - orderId: 159653829, - origQty: '1.00000000', - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'NEW', - type: 'MARKET', - side: 'BUY', - nextCheck: moment() - .add(5, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') - } - } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'FILLED', - type: 'MARKET', - side: 'BUY' - }, - expectedCalculateLastBuyPrice: false, - expectedFilledOrder: false - }, - { - desc: 'with MARKET order and FILLED, but not yet to check', - symbol: 'CAKEUSDT', - lastBuyPriceDoc: { - lastBuyPrice: 30, - quantity: 3 - }, - featureToggle: { notifyDebug: false }, - orderId: 159653829, - cacheResults: [ - { - order: { - symbol: 'CAKEUSDT', - orderId: 159653829, - origQty: '1.00000000', - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'NEW', - type: 'MARKET', - side: 'SELL', - nextCheck: moment() - .add(5, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') - } - } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'FILLED', - type: 'MARKET', - side: 'SELL' - }, - expectedCalculateLastBuyPrice: false, - expectedFilledOrder: false - } - ].forEach(testData => { - describe(`${testData.desc}`, () => { - beforeEach(async () => { - mockGetManualOrders = jest - .fn() - .mockResolvedValue(testData.cacheResults); - - jest.mock('../../../trailingTradeHelper/order', () => ({ - getManualOrders: mockGetManualOrders, - deleteManualOrder: mockDeleteManualOrder, - saveManualOrder: mockSaveManualOrder - })); - - binanceMock.client.getOrder = jest - .fn() - .mockResolvedValue(testData.getOrderResult); - - const step = require('../ensure-manual-order'); - - rawData = { - symbol: testData.symbol, - featureToggle: testData.featureToggle, - isLocked: false, - symbolConfiguration: { - system: { - checkManualOrderPeriod: 10 - } - } - }; - - result = await step.execute(loggerMock, rawData); - }); - - if (testData.expectedCalculateLastBuyPrice) { - it('triggers calculateLastBuyPrice', () => { - expect(mockCalculateLastBuyPrice).toHaveBeenCalledWith( - loggerMock, - testData.symbol, - testData.getOrderResult - ); - }); - } else { - it('does not trigger calculateLastBuyPrice', () => { - expect(mockCalculateLastBuyPrice).not.toHaveBeenCalled(); - }); - } - - if (testData.expectedFilledOrder) { - it('triggers deleteManualOrder', () => { - expect(mockDeleteManualOrder).toHaveBeenCalledWith( - loggerMock, - testData.symbol, - testData.orderId - ); - }); - - it('triggers getSymbolGridTrade', () => { - expect(mockGetSymbolGridTrade).toHaveBeenCalledWith( - loggerMock, - testData.symbol - ); - }); - - it('triggers saveSymbolGridTrade', () => { - expect(mockSaveSymbolGridTrade).toHaveBeenCalledWith( - loggerMock, - testData.symbol, - { - buy: [ - { - some: 'value' - } - ], - sell: [{ some: 'value' }], - manualTrade: [testData.getOrderResult] - } - ); - }); - } else { - it('does not trigger deleteManualOrder', () => { - expect(mockDeleteManualOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger getSymbolGridTrade', () => { - expect(mockGetSymbolGridTrade).not.toHaveBeenCalled(); - }); - - it('does not trigger saveSymbolGridTrade', () => { - expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); - }); - } - - it('does not trigger saveManualOrder', () => { - expect(mockSaveManualOrder).not.toHaveBeenCalled(); - }); - }); - }); - [ { desc: 'with LIMIT order and CANCELED', @@ -705,24 +390,12 @@ describe('ensure-manual-order.js', () => { origQty: '1.00000000', executedQty: '1.00000000', cummulativeQuoteQty: '19.54900000', - status: 'NEW', + status: 'CANCELED', type: 'LIMIT', - side: 'BUY', - nextCheck: moment() - .subtract(1, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') + side: 'BUY' } } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'CANCELED', - type: 'LIMIT', - side: 'BUY' - } + ] }, { desc: 'with LIMIT order and REJECTED', @@ -736,24 +409,12 @@ describe('ensure-manual-order.js', () => { origQty: '1.00000000', executedQty: '1.00000000', cummulativeQuoteQty: '19.54900000', - status: 'NEW', + status: 'REJECTED', type: 'LIMIT', - side: 'BUY', - nextCheck: moment() - .subtract(1, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') + side: 'BUY' } } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'REJECTED', - type: 'LIMIT', - side: 'BUY' - } + ] }, { desc: 'with LIMIT order and EXPIRED', @@ -767,24 +428,12 @@ describe('ensure-manual-order.js', () => { origQty: '1.00000000', executedQty: '1.00000000', cummulativeQuoteQty: '19.54900000', - status: 'NEW', + status: 'EXPIRED', type: 'LIMIT', - side: 'BUY', - nextCheck: moment() - .subtract(1, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') + side: 'BUY' } } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'EXPIRED', - type: 'LIMIT', - side: 'BUY' - } + ] }, { desc: 'with LIMIT order and PENDING_CANCEL', @@ -798,24 +447,12 @@ describe('ensure-manual-order.js', () => { origQty: '1.00000000', executedQty: '1.00000000', cummulativeQuoteQty: '19.54900000', - status: 'NEW', + status: 'PENDING_CANCEL', type: 'LIMIT', - side: 'BUY', - nextCheck: moment() - .subtract(1, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') + side: 'BUY' } } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'PENDING_CANCEL', - type: 'LIMIT', - side: 'BUY' - } + ] }, { desc: 'with LIMIT order and CANCELED', @@ -829,24 +466,12 @@ describe('ensure-manual-order.js', () => { origQty: '1.00000000', executedQty: '1.00000000', cummulativeQuoteQty: '19.54900000', - status: 'NEW', + status: 'CANCELED', type: 'LIMIT', - side: 'BUY', - nextCheck: moment() - .subtract(1, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') + side: 'BUY' } } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'CANCELED', - type: 'LIMIT', - side: 'BUY' - } + ] } ].forEach(testData => { describe(`${testData.desc}`, () => { @@ -857,14 +482,9 @@ describe('ensure-manual-order.js', () => { jest.mock('../../../trailingTradeHelper/order', () => ({ getManualOrders: mockGetManualOrders, - deleteManualOrder: mockDeleteManualOrder, - saveManualOrder: mockSaveManualOrder + deleteManualOrder: mockDeleteManualOrder })); - binanceMock.client.getOrder = jest - .fn() - .mockResolvedValue(testData.getOrderResult); - const step = require('../ensure-manual-order'); rawData = { @@ -900,16 +520,12 @@ describe('ensure-manual-order.js', () => { it('does not trigger saveSymbolGridTrade', () => { expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); }); - - it('does not trigger saveManualOrder', () => { - expect(mockSaveManualOrder).not.toHaveBeenCalled(); - }); }); }); [ { - desc: 'with LIMIT order and still NEW', + desc: 'with LIMIT order and NEW', symbol: 'CAKEUSDT', orderId: 159653829, cacheResults: [ @@ -922,22 +538,10 @@ describe('ensure-manual-order.js', () => { cummulativeQuoteQty: '19.54900000', status: 'NEW', type: 'LIMIT', - side: 'BUY', - nextCheck: moment() - .subtract(1, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') + side: 'BUY' } } - ], - getOrderResult: { - symbol: 'CAKEUSDT', - orderId: 159653829, - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'NEW', - type: 'LIMIT', - side: 'BUY' - } + ] } ].forEach(testData => { describe(`${testData.desc}`, () => { @@ -948,14 +552,9 @@ describe('ensure-manual-order.js', () => { jest.mock('../../../trailingTradeHelper/order', () => ({ getManualOrders: mockGetManualOrders, - deleteManualOrder: mockDeleteManualOrder, - saveManualOrder: mockSaveManualOrder + deleteManualOrder: mockDeleteManualOrder })); - binanceMock.client.getOrder = jest - .fn() - .mockResolvedValue(testData.getOrderResult); - const step = require('../ensure-manual-order'); rawData = { @@ -980,18 +579,6 @@ describe('ensure-manual-order.js', () => { expect(mockDeleteManualOrder).not.toHaveBeenCalled(); }); - it('triggers saveManualOrder', () => { - expect(mockSaveManualOrder).toHaveBeenCalledWith( - loggerMock, - testData.symbol, - testData.orderId, - { - ...testData.getOrderResult, - nextCheck: expect.any(String) - } - ); - }); - it('does not trigger getSymbolGridTrade', () => { expect(mockGetSymbolGridTrade).not.toHaveBeenCalled(); }); @@ -1001,88 +588,6 @@ describe('ensure-manual-order.js', () => { }); }); }); - - describe('when binance.client.getOrder throws an error', () => { - beforeEach(async () => { - mockGetManualOrders = jest.fn().mockResolvedValue([ - { - order: { - symbol: 'CAKEUSDT', - orderId: 159653829, - origQty: '1.00000000', - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'NEW', - type: 'LIMIT', - side: 'BUY', - nextCheck: moment() - .subtract(1, 'minute') - .format('YYYY-MM-DDTHH:mm:ssZ') - } - } - ]); - - jest.mock('../../../trailingTradeHelper/order', () => ({ - getManualOrders: mockGetManualOrders, - deleteManualOrder: mockDeleteManualOrder, - saveManualOrder: mockSaveManualOrder - })); - - binanceMock.client.getOrder = jest - .fn() - .mockRejectedValue(new Error('Order is not found.')); - - const step = require('../ensure-manual-order'); - - rawData = { - symbol: 'CAKEUSDT', - featureToggle: { notifyDebug: true }, - isLocked: false, - symbolConfiguration: { - system: { - checkManualOrderPeriod: 10 - } - } - }; - - result = await step.execute(loggerMock, rawData); - }); - - it('does not trigger calculateLastBuyPrice', () => { - expect(mockCalculateLastBuyPrice).not.toHaveBeenCalled(); - }); - - it('does not trigger deleteManualOrder', () => { - expect(mockDeleteManualOrder).not.toHaveBeenCalled(); - }); - - it('does not trigger getSymbolGridTrade', () => { - expect(mockGetSymbolGridTrade).not.toHaveBeenCalled(); - }); - - it('does not trigger saveSymbolGridTrade', () => { - expect(mockSaveSymbolGridTrade).not.toHaveBeenCalled(); - }); - - it('triggers saveManualOrder', () => { - expect(mockSaveManualOrder).toHaveBeenCalledWith( - loggerMock, - 'CAKEUSDT', - 159653829, - { - symbol: 'CAKEUSDT', - orderId: 159653829, - origQty: '1.00000000', - executedQty: '1.00000000', - cummulativeQuoteQty: '19.54900000', - status: 'NEW', - type: 'LIMIT', - side: 'BUY', - nextCheck: expect.any(String) - } - ); - }); - }); }); }); }); diff --git a/app/cronjob/trailingTrade/step/__tests__/get-balances.test.js b/app/cronjob/trailingTrade/step/__tests__/get-balances.test.js index 9cbd71e6..9a73afe1 100644 --- a/app/cronjob/trailingTrade/step/__tests__/get-balances.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/get-balances.test.js @@ -38,7 +38,7 @@ describe('determine-action.js', () => { locked: '0.00000000', total: 0.1005, estimatedValue: 194.568, - updatedAt: expect.any(String) + updatedAt: expect.any(Date) }, quoteAssetBalance: { asset: 'USDT', @@ -76,7 +76,7 @@ describe('determine-action.js', () => { locked: '0.00000000', total: 0.1005, estimatedValue: 0, - updatedAt: expect.any(String) + updatedAt: expect.any(Date) }, quoteAssetBalance: { asset: 'USDT', @@ -115,7 +115,7 @@ describe('determine-action.js', () => { locked: 0, total: 0, estimatedValue: 0, - updatedAt: expect.any(String) + updatedAt: expect.any(Date) }, quoteAssetBalance: { asset: 'TUSD', diff --git a/app/cronjob/trailingTrade/step/__tests__/get-indicators.test.js b/app/cronjob/trailingTrade/step/__tests__/get-indicators.test.js index 371ba173..487bf506 100644 --- a/app/cronjob/trailingTrade/step/__tests__/get-indicators.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/get-indicators.test.js @@ -7,6 +7,7 @@ describe('get-indicators.js', () => { let cacheMock; let loggerMock; + let mongoMock; let mockGetLastBuyPrice; @@ -14,9 +15,10 @@ describe('get-indicators.js', () => { beforeEach(() => { jest.clearAllMocks().resetModules(); - const { cache, logger } = require('../../../../helpers'); + const { cache, logger, mongo } = require('../../../../helpers'); cacheMock = cache; loggerMock = logger; + mongoMock = mongo; }); describe('with no open orders and no last buy price', () => { @@ -27,17 +29,6 @@ describe('get-indicators.js', () => { })); cacheMock.hget = jest.fn().mockImplementation((hash, key) => { - if ( - hash === 'trailing-trade-symbols' && - key === 'BTCUSDT-indicator-data' - ) { - return JSON.stringify({ - highestPrice: 10000, - lowestPrice: 8893.03, - athPrice: 9000 - }); - } - if ( hash === 'trailing-trade-symbols' && key === 'BTCUSDT-latest-candle' @@ -70,6 +61,49 @@ describe('get-indicators.js', () => { return null; }); + mongoMock.findAll = jest + .fn() + .mockImplementation((_logger, collectionName, _query, _params) => { + if (collectionName === 'trailing-trade-candles') { + return [ + { + interval: '1h', + key: 'BTCUSDT', + open: 8990.5, + high: 10000, + low: 8893.03, + close: 9899.05 + }, + { + interval: '1h', + key: 'BTCUSDT', + open: 8666.4, + high: 9000.6, + low: 8899.03, + close: 9000.1 + } + ]; + } + return [ + { + interval: '1d', + key: 'BTCUSDT', + open: 8690.5, + high: 9000, + low: 8110.04, + close: 9899.05 + }, + { + interval: '1d', + key: 'BTCUSDT', + open: 7755.66, + high: 8000, + low: 7695.6, + close: 8500 + } + ]; + }); + step = require('../get-indicators'); rawData = { @@ -78,6 +112,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -86,7 +121,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -115,6 +154,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -123,7 +163,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -206,17 +250,6 @@ describe('get-indicators.js', () => { })); cacheMock.hget = jest.fn().mockImplementation((hash, key) => { - if ( - hash === 'trailing-trade-symbols' && - key === 'BTCUSDT-indicator-data' - ) { - return JSON.stringify({ - highestPrice: 10000, - lowestPrice: 8893.03, - athPrice: null - }); - } - if ( hash === 'trailing-trade-symbols' && key === 'BTCUSDT-latest-candle' @@ -249,6 +282,49 @@ describe('get-indicators.js', () => { return null; }); + mongoMock.findAll = jest + .fn() + .mockImplementation((_logger, collectionName, _query, _params) => { + if (collectionName === 'trailing-trade-candles') { + return [ + { + interval: '1h', + key: 'BTCUSDT', + open: 8990.5, + high: 10000, + low: 8893.03, + close: 9899.05 + }, + { + interval: '1h', + key: 'BTCUSDT', + open: 8666.4, + high: 9000.6, + low: 8899.03, + close: 9000.1 + } + ]; + } + return [ + { + interval: '1d', + key: 'BTCUSDT', + open: 8690.5, + high: 9000, + low: 8110.04, + close: 9899.05 + }, + { + interval: '1d', + key: 'BTCUSDT', + open: 7755.66, + high: 8000, + low: 7695.6, + close: 8500 + } + ]; + }); + step = require('../get-indicators'); rawData = { @@ -257,6 +333,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -265,7 +342,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: false, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -294,6 +375,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -302,7 +384,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: false, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -386,17 +472,6 @@ describe('get-indicators.js', () => { getLastBuyPrice: mockGetLastBuyPrice })); cacheMock.hget = jest.fn().mockImplementation((hash, key) => { - if ( - hash === 'trailing-trade-symbols' && - key === 'BTCUSDT-indicator-data' - ) { - return JSON.stringify({ - highestPrice: 10000, - lowestPrice: 8893.03, - athPrice: 9000 - }); - } - if ( hash === 'trailing-trade-symbols' && key === 'BTCUSDT-latest-candle' @@ -428,6 +503,49 @@ describe('get-indicators.js', () => { return null; }); + + mongoMock.findAll = jest + .fn() + .mockImplementation((_logger, collectionName, _query, _params) => { + if (collectionName === 'trailing-trade-candles') { + return [ + { + interval: '1h', + key: 'BTCUSDT', + open: 8990.5, + high: 10000, + low: 8893.03, + close: 9899.05 + }, + { + interval: '1h', + key: 'BTCUSDT', + open: 8666.4, + high: 9000.6, + low: 8899.03, + close: 9000.1 + } + ]; + } + return [ + { + interval: '1d', + key: 'BTCUSDT', + open: 8690.5, + high: 9000, + low: 8110.04, + close: 9899.05 + }, + { + interval: '1d', + key: 'BTCUSDT', + open: 7755.66, + high: 8000, + low: 7695.6, + close: 8500 + } + ]; + }); }); describe('when buy grid trade index is null', () => { @@ -440,12 +558,17 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: -1, currentGridTrade: null, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -474,12 +597,17 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: -1, currentGridTrade: null, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -561,6 +689,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -569,7 +698,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -601,6 +734,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -609,7 +743,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -694,6 +832,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 1, currentGridTrade: { @@ -702,7 +841,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -734,6 +877,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 1, currentGridTrade: { @@ -742,7 +886,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -828,17 +976,6 @@ describe('get-indicators.js', () => { })); cacheMock.hget = jest.fn().mockImplementation((hash, key) => { - if ( - hash === 'trailing-trade-symbols' && - key === 'BTCUSDT-indicator-data' - ) { - return JSON.stringify({ - highestPrice: 10000, - lowestPrice: 8893.03, - athPrice: 9000 - }); - } - if ( hash === 'trailing-trade-symbols' && key === 'BTCUSDT-latest-candle' @@ -870,6 +1007,49 @@ describe('get-indicators.js', () => { return null; }); + + mongoMock.findAll = jest + .fn() + .mockImplementation((_logger, collectionName, _query, _params) => { + if (collectionName === 'trailing-trade-candles') { + return [ + { + interval: '1h', + key: 'BTCUSDT', + open: 8990.5, + high: 10000, + low: 8893.03, + close: 9899.05 + }, + { + interval: '1h', + key: 'BTCUSDT', + open: 8666.4, + high: 9000.6, + low: 8899.03, + close: 9000.1 + } + ]; + } + return [ + { + interval: '1d', + key: 'BTCUSDT', + open: 8690.5, + high: 9000, + low: 8110.04, + close: 9899.05 + }, + { + interval: '1d', + key: 'BTCUSDT', + open: 7755.66, + high: 8000, + low: 7695.6, + close: 8500 + } + ]; + }); }); describe('when buy grid trade index is null', () => { @@ -882,12 +1062,17 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: -1, currentGridTrade: null, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -946,12 +1131,17 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: -1, currentGridTrade: null, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -1118,6 +1308,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -1126,7 +1317,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -1188,6 +1383,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -1196,7 +1392,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -1366,6 +1566,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 1, currentGridTrade: { @@ -1374,7 +1575,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -1436,6 +1641,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 1, currentGridTrade: { @@ -1444,7 +1650,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -1613,17 +1823,6 @@ describe('get-indicators.js', () => { })); cacheMock.hget = jest.fn().mockImplementation((hash, key) => { - if ( - hash === 'trailing-trade-symbols' && - key === 'BTCUSDT-indicator-data' - ) { - return JSON.stringify({ - highestPrice: 10000, - lowestPrice: 8893.03, - athPrice: 9000 - }); - } - if ( hash === 'trailing-trade-symbols' && key === 'BTCUSDT-latest-candle' @@ -1656,6 +1855,49 @@ describe('get-indicators.js', () => { return null; }); + mongoMock.findAll = jest + .fn() + .mockImplementation((_logger, collectionName, _query, _params) => { + if (collectionName === 'trailing-trade-candles') { + return [ + { + interval: '1h', + key: 'BTCUSDT', + open: 8990.5, + high: 10000, + low: 8893.03, + close: 9899.05 + }, + { + interval: '1h', + key: 'BTCUSDT', + open: 8666.4, + high: 9000.6, + low: 8899.03, + close: 9000.1 + } + ]; + } + return [ + { + interval: '1d', + key: 'BTCUSDT', + open: 8690.5, + high: 9000, + low: 8110.04, + close: 9899.05 + }, + { + interval: '1d', + key: 'BTCUSDT', + open: 7755.66, + high: 8000, + low: 7695.6, + close: 8500 + } + ]; + }); + step = require('../get-indicators'); rawData = { symbol: 'BTCUSDT', @@ -1663,6 +1905,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -1671,7 +1914,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -1728,6 +1975,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -1736,7 +1984,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -1904,17 +2156,6 @@ describe('get-indicators.js', () => { })); cacheMock.hget = jest.fn().mockImplementation((hash, key) => { - if ( - hash === 'trailing-trade-symbols' && - key === 'BTCUSDT-indicator-data' - ) { - return JSON.stringify({ - highestPrice: 10000, - lowestPrice: 8893.03, - athPrice: 9000 - }); - } - if ( hash === 'trailing-trade-symbols' && key === 'BTCUSDT-latest-candle' @@ -1947,6 +2188,49 @@ describe('get-indicators.js', () => { return null; }); + mongoMock.findAll = jest + .fn() + .mockImplementation((_logger, collectionName, _query, _params) => { + if (collectionName === 'trailing-trade-candles') { + return [ + { + interval: '1h', + key: 'BTCUSDT', + open: 8990.5, + high: 10000, + low: 8893.03, + close: 9899.05 + }, + { + interval: '1h', + key: 'BTCUSDT', + open: 8666.4, + high: 9000.6, + low: 8899.03, + close: 9000.1 + } + ]; + } + return [ + { + interval: '1d', + key: 'BTCUSDT', + open: 8690.5, + high: 9000, + low: 8110.04, + close: 9899.05 + }, + { + interval: '1d', + key: 'BTCUSDT', + open: 7755.66, + high: 8000, + low: 7695.6, + close: 8500 + } + ]; + }); + step = require('../get-indicators'); rawData = { @@ -1955,6 +2239,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -1963,7 +2248,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -1996,6 +2285,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -2004,7 +2294,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -2079,7 +2373,7 @@ describe('get-indicators.js', () => { }); }); - describe('when there is no indicator data cache', () => { + describe('when there are no candles from mongo', () => { beforeEach(async () => { mockGetLastBuyPrice = jest.fn().mockResolvedValue(null); jest.mock('../../../trailingTradeHelper/common', () => ({ @@ -2100,6 +2394,8 @@ describe('get-indicators.js', () => { return null; }); + mongoMock.findAll = jest.fn().mockResolvedValue([]); + step = require('../get-indicators'); rawData = { @@ -2108,6 +2404,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -2116,7 +2413,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -2141,6 +2442,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -2149,7 +2451,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -2174,18 +2480,50 @@ describe('get-indicators.js', () => { getLastBuyPrice: mockGetLastBuyPrice })); - cacheMock.hget = jest.fn().mockImplementation((hash, key) => { - if ( - hash === 'trailing-trade-symbols' && - key === 'BTCUSDT-indicator-data' - ) { - return JSON.stringify({ - lowestPrice: 8893.03 - }); - } + cacheMock.hget = jest.fn().mockResolvedValue(null); - return null; - }); + mongoMock.findAll = jest + .fn() + .mockImplementation((_logger, collectionName, _query, _params) => { + if (collectionName === 'trailing-trade-candles') { + return [ + { + interval: '1h', + key: 'BTCUSDT', + open: 8990.5, + high: 10000, + low: 8893.03, + close: 9899.05 + }, + { + interval: '1h', + key: 'BTCUSDT', + open: 8666.4, + high: 9000.6, + low: 8899.03, + close: 9000.1 + } + ]; + } + return [ + { + interval: '1d', + key: 'BTCUSDT', + open: 8690.5, + high: 9000, + low: 8110.04, + close: 9899.05 + }, + { + interval: '1d', + key: 'BTCUSDT', + open: 7755.66, + high: 8000, + low: 7695.6, + close: 8500 + } + ]; + }); step = require('../get-indicators'); @@ -2195,6 +2533,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -2203,7 +2542,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -2228,6 +2571,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -2236,7 +2580,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -2262,17 +2610,6 @@ describe('get-indicators.js', () => { })); cacheMock.hget = jest.fn().mockImplementation((hash, key) => { - if ( - hash === 'trailing-trade-symbols' && - key === 'BTCUSDT-indicator-data' - ) { - return JSON.stringify({ - highestPrice: 10000, - lowestPrice: 8893.03, - athPrice: 9000 - }); - } - if ( hash === 'trailing-trade-symbols' && key === 'BTCUSDT-latest-candle' @@ -2286,6 +2623,49 @@ describe('get-indicators.js', () => { return null; }); + mongoMock.findAll = jest + .fn() + .mockImplementation((_logger, collectionName, _query, _params) => { + if (collectionName === 'trailing-trade-candles') { + return [ + { + interval: '1h', + key: 'BTCUSDT', + open: 8990.5, + high: 10000, + low: 8893.03, + close: 9899.05 + }, + { + interval: '1h', + key: 'BTCUSDT', + open: 8666.4, + high: 9000.6, + low: 8899.03, + close: 9000.1 + } + ]; + } + return [ + { + interval: '1d', + key: 'BTCUSDT', + open: 8690.5, + high: 9000, + low: 8110.04, + close: 9899.05 + }, + { + interval: '1d', + key: 'BTCUSDT', + open: 7755.66, + high: 8000, + low: 7695.6, + close: 8500 + } + ]; + }); + step = require('../get-indicators'); rawData = { symbol: 'BTCUSDT', @@ -2293,6 +2673,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -2301,7 +2682,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { @@ -2328,6 +2713,7 @@ describe('get-indicators.js', () => { filterMinNotional: { minNotional: '10.000' } }, symbolConfiguration: { + candles: { limit: '100' }, buy: { currentGridTradeIndex: 0, currentGridTrade: { @@ -2336,7 +2722,11 @@ describe('get-indicators.js', () => { }, athRestriction: { enabled: true, - restrictionPercentage: 0.9 + restrictionPercentage: 0.9, + candles: { + interval: '1d', + limit: 30 + } } }, sell: { diff --git a/app/cronjob/trailingTrade/step/__tests__/handle-open-orders.test.js b/app/cronjob/trailingTrade/step/__tests__/handle-open-orders.test.js index d05b488c..87c872d0 100644 --- a/app/cronjob/trailingTrade/step/__tests__/handle-open-orders.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/handle-open-orders.test.js @@ -9,8 +9,9 @@ describe('handle-open-orders.js', () => { let loggerMock; let slackMock; - let mockGetAccountInfoFromAPI; + let mockGetAccountInfo; let mockGetAndCacheOpenOrdersForSymbol; + let mockUpdateAccountInfo; let mockSaveOverrideAction; const accountInfoJSON = require('./fixtures/binance-account-info.json'); @@ -19,26 +20,29 @@ describe('handle-open-orders.js', () => { beforeEach(() => { jest.clearAllMocks().resetModules(); + const { binance, logger, slack } = require('../../../../helpers'); + binanceMock = binance; + loggerMock = logger; + slackMock = slack; + + mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); + binanceMock.client.cancelOrder = jest.fn().mockResolvedValue(true); + mockSaveOverrideAction = jest.fn().mockResolvedValue(true); + mockUpdateAccountInfo = jest.fn().mockResolvedValue({ + account: 'updated' + }); }); describe('when symbol is locked', () => { beforeEach(async () => { - const { binance, logger } = require('../../../../helpers'); - binanceMock = binance; - loggerMock = logger; - binanceMock.client.cancelOrder = jest.fn().mockResolvedValue(true); - - mockGetAccountInfoFromAPI = jest - .fn() - .mockResolvedValue(accountInfoJSON); - - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); + mockGetAccountInfo = jest.fn().mockResolvedValue(accountInfoJSON); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - saveOverrideAction: mockSaveOverrideAction + getAccountInfo: mockGetAccountInfo, + updateAccountInfo: mockUpdateAccountInfo, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); step = require('../handle-open-orders'); @@ -71,10 +75,13 @@ describe('handle-open-orders.js', () => { sell: { limitPrice: 1800, openOrders: [] + }, + symbolInfo: { + quoteAsset: 'USDT' } }; - result = await step.execute(logger, rawData); + result = await step.execute(loggerMock, rawData); }); it('does not trigger cancelOrder', () => { @@ -85,8 +92,8 @@ describe('handle-open-orders.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('returns expected value', () => { @@ -115,28 +122,25 @@ describe('handle-open-orders.js', () => { } ] }, - sell: { limitPrice: 1800, openOrders: [] } + sell: { limitPrice: 1800, openOrders: [] }, + symbolInfo: { + quoteAsset: 'USDT' + } }); }); }); describe('when action is not not-determined', () => { beforeEach(async () => { - const { binance, logger } = require('../../../../helpers'); - binanceMock = binance; - loggerMock = logger; - binanceMock.client.cancelOrder = jest.fn().mockResolvedValue(true); - - mockGetAccountInfoFromAPI = jest - .fn() - .mockResolvedValue(accountInfoJSON); + mockGetAccountInfo = jest.fn().mockResolvedValue(accountInfoJSON); mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - saveOverrideAction: mockSaveOverrideAction + getAccountInfo: mockGetAccountInfo, + updateAccountInfo: mockUpdateAccountInfo, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); step = require('../handle-open-orders'); @@ -169,10 +173,13 @@ describe('handle-open-orders.js', () => { sell: { limitPrice: 1800, openOrders: [] + }, + symbolInfo: { + quoteAsset: 'USDT' } }; - result = await step.execute(logger, rawData); + result = await step.execute(loggerMock, rawData); }); it('does not trigger cancelOrder', () => { @@ -183,8 +190,8 @@ describe('handle-open-orders.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('returns expected value', () => { @@ -213,28 +220,25 @@ describe('handle-open-orders.js', () => { } ] }, - sell: { limitPrice: 1800, openOrders: [] } + sell: { limitPrice: 1800, openOrders: [] }, + symbolInfo: { + quoteAsset: 'USDT' + } }); }); }); describe('when order is not STOP_LOSS_LIMIT', () => { beforeEach(async () => { - const { binance, logger } = require('../../../../helpers'); - binanceMock = binance; - loggerMock = logger; - binanceMock.client.cancelOrder = jest.fn().mockResolvedValue(true); - - mockGetAccountInfoFromAPI = jest - .fn() - .mockResolvedValue(accountInfoJSON); + mockGetAccountInfo = jest.fn().mockResolvedValue(accountInfoJSON); mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - saveOverrideAction: mockSaveOverrideAction + getAccountInfo: mockGetAccountInfo, + updateAccountInfo: mockUpdateAccountInfo, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); step = require('../handle-open-orders'); @@ -267,10 +271,13 @@ describe('handle-open-orders.js', () => { sell: { limitPrice: 1800, openOrders: [] + }, + symbolInfo: { + quoteAsset: 'USDT' } }; - result = await step.execute(logger, rawData); + result = await step.execute(loggerMock, rawData); }); it('does not trigger cancelOrder', () => { @@ -281,8 +288,8 @@ describe('handle-open-orders.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('returns expected value', () => { @@ -311,7 +318,10 @@ describe('handle-open-orders.js', () => { } ] }, - sell: { limitPrice: 1800, openOrders: [] } + sell: { limitPrice: 1800, openOrders: [] }, + symbolInfo: { + quoteAsset: 'USDT' + } }); }); }); @@ -319,71 +329,53 @@ describe('handle-open-orders.js', () => { describe('when order is buy', () => { describe('when stop price is higher or equal than current limit price', () => { describe('when cancelling order is failed', () => { - beforeEach(async () => { - const { binance, logger, slack } = require('../../../../helpers'); - binanceMock = binance; - loggerMock = logger; - slackMock = slack; - + beforeEach(() => { slackMock.sendMessage = jest.fn().mockResolvedValue(true); binanceMock.client.cancelOrder = jest .fn() .mockRejectedValue(new Error('something happened')); - mockGetAccountInfoFromAPI = jest - .fn() - .mockResolvedValue(accountInfoJSON); - - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([ - { - symbol: 'BTCUSDT', - orderId: 46839, - price: '1799.58000000', - stopPrice: '1800.1000', - type: 'STOP_LOSS_LIMIT', - side: 'BUY' - }, - { - symbol: 'BTCUSDT', - orderId: 46841, - price: '1799.58000000', - stopPrice: '1800.1000', - type: 'STOP_LOSS_LIMIT', - side: 'SELL' - } - ]); + mockGetAccountInfo = jest.fn().mockResolvedValue(accountInfoJSON); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - saveOverrideAction: mockSaveOverrideAction + getAccountInfo: mockGetAccountInfo, + updateAccountInfo: mockUpdateAccountInfo, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); - - step = require('../handle-open-orders'); }); - beforeEach(async () => { - rawData = { - symbol: 'BTCUSDT', - isLocked: false, - featureToggle: { - notifyDebug: false - }, - action: 'not-determined', - openOrders: [ + describe('when got getAndCacheOpenOrdersForSymbol successfully', () => { + beforeEach(async () => { + mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([ { symbol: 'BTCUSDT', - orderId: 46838, + orderId: 46839, price: '1799.58000000', stopPrice: '1800.1000', type: 'STOP_LOSS_LIMIT', side: 'BUY' + }, + { + symbol: 'BTCUSDT', + orderId: 46841, + price: '1799.58000000', + stopPrice: '1800.1000', + type: 'STOP_LOSS_LIMIT', + side: 'SELL' } - ], - buy: { - limitPrice: 1800, + ]); + + step = require('../handle-open-orders'); + + rawData = { + symbol: 'BTCUSDT', + isLocked: false, + featureToggle: { + notifyDebug: false + }, + action: 'not-determined', openOrders: [ { symbol: 'BTCUSDT', @@ -393,79 +385,81 @@ describe('handle-open-orders.js', () => { type: 'STOP_LOSS_LIMIT', side: 'BUY' } - ] - }, - sell: { - limitPrice: 1800, - openOrders: [] - } - }; - - result = await step.execute(loggerMock, rawData); - }); + ], + buy: { + limitPrice: 1800, + openOrders: [ + { + symbol: 'BTCUSDT', + orderId: 46838, + price: '1799.58000000', + stopPrice: '1800.1000', + type: 'STOP_LOSS_LIMIT', + side: 'BUY' + } + ] + }, + sell: { + limitPrice: 1800, + openOrders: [] + }, + symbolInfo: { + quoteAsset: 'USDT' + } + }; - it('triggers cancelOrder', () => { - expect(binanceMock.client.cancelOrder).toHaveBeenCalledWith({ - symbol: 'BTCUSDT', - orderId: 46838 + result = await step.execute(loggerMock, rawData); }); - }); - it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); - }); + it('triggers cancelOrder', () => { + expect(binanceMock.client.cancelOrder).toHaveBeenCalledWith({ + symbol: 'BTCUSDT', + orderId: 46838 + }); + }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalledWith(loggerMock); - }); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalledWith(loggerMock); + }); - it('does not trigger slack.sendMessage', () => { - expect(slackMock.sendMessage).not.toHaveBeenCalled(); - }); + it('triggers getAndCacheOpenOrdersForSymbol', () => { + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); - it('triggers saveOverrideAction', () => { - expect(mockSaveOverrideAction).toHaveBeenCalledWith( - loggerMock, - 'BTCUSDT', - { - action: 'buy', - actionAt: expect.any(String), - triggeredBy: 'buy-cancelled', - notify: false, - checkTradingView: true - }, - 'The bot will place a buy order in the next tick because could not retrieve the cancelled order result.' - ); - }); + it('does not trigger slack.sendMessage', () => { + expect(slackMock.sendMessage).not.toHaveBeenCalled(); + }); - it('returns expected value', () => { - expect(result).toStrictEqual({ - symbol: 'BTCUSDT', - isLocked: false, - featureToggle: { - notifyDebug: false - }, - action: 'buy-order-checking', - openOrders: [ + it('triggers saveOverrideAction', () => { + expect(mockSaveOverrideAction).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT', { - symbol: 'BTCUSDT', - orderId: 46839, - price: '1799.58000000', - stopPrice: '1800.1000', - type: 'STOP_LOSS_LIMIT', - side: 'BUY' + action: 'buy', + actionAt: expect.any(String), + triggeredBy: 'buy-cancelled', + notify: false, + checkTradingView: true }, - { - symbol: 'BTCUSDT', - orderId: 46841, - price: '1799.58000000', - stopPrice: '1800.1000', - type: 'STOP_LOSS_LIMIT', - side: 'SELL' - } - ], - buy: { - limitPrice: 1800, + 'The bot will place a buy order in the next tick because could not retrieve the cancelled order result.' + ); + }); + + it('does not trigger updateAccountInfo', () => { + expect(mockUpdateAccountInfo).not.toHaveBeenCalled(); + }); + + it('returns expected value', () => { + expect(result).toStrictEqual({ + symbol: 'BTCUSDT', + isLocked: false, + featureToggle: { + notifyDebug: false + }, + action: 'buy-order-checking', openOrders: [ { symbol: 'BTCUSDT', @@ -474,35 +468,52 @@ describe('handle-open-orders.js', () => { stopPrice: '1800.1000', type: 'STOP_LOSS_LIMIT', side: 'BUY' + }, + { + symbol: 'BTCUSDT', + orderId: 46841, + price: '1799.58000000', + stopPrice: '1800.1000', + type: 'STOP_LOSS_LIMIT', + side: 'SELL' } - ] - }, - sell: { limitPrice: 1800, openOrders: [] }, - accountInfo: accountInfoJSON + ], + buy: { + limitPrice: 1800, + openOrders: [ + { + symbol: 'BTCUSDT', + orderId: 46839, + price: '1799.58000000', + stopPrice: '1800.1000', + type: 'STOP_LOSS_LIMIT', + side: 'BUY' + } + ] + }, + sell: { limitPrice: 1800, openOrders: [] }, + symbolInfo: { + quoteAsset: 'USDT' + }, + accountInfo: accountInfoJSON + }); }); }); }); describe('when cancelling order is succeed', () => { beforeEach(async () => { - const { binance, logger } = require('../../../../helpers'); - binanceMock = binance; - loggerMock = logger; - binanceMock.client.cancelOrder = jest.fn().mockResolvedValue(true); - - mockGetAccountInfoFromAPI = jest - .fn() - .mockResolvedValue(accountInfoJSON); + mockGetAccountInfo = jest.fn().mockResolvedValue(accountInfoJSON); mockGetAndCacheOpenOrdersForSymbol = jest .fn() .mockResolvedValue([]); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - saveOverrideAction: mockSaveOverrideAction + getAccountInfo: mockGetAccountInfo, + updateAccountInfo: mockUpdateAccountInfo, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); step = require('../handle-open-orders'); @@ -515,6 +526,7 @@ describe('handle-open-orders.js', () => { { symbol: 'BTCUSDT', orderId: 46838, + origQty: '0.00001', price: '1799.58000000', stopPrice: '1800.1000', type: 'STOP_LOSS_LIMIT', @@ -527,6 +539,7 @@ describe('handle-open-orders.js', () => { { symbol: 'BTCUSDT', orderId: 46838, + origQty: '0.00001', price: '1799.58000000', stopPrice: '1800.1000', type: 'STOP_LOSS_LIMIT', @@ -537,10 +550,17 @@ describe('handle-open-orders.js', () => { sell: { limitPrice: 1800, openOrders: [] + }, + symbolInfo: { + quoteAsset: 'USDT' + }, + quoteAssetBalance: { + free: 50, + locked: 0.0179958 } }; - result = await step.execute(logger, rawData); + result = await step.execute(loggerMock, rawData); }); it('triggers cancelOrder', () => { @@ -554,8 +574,22 @@ describe('handle-open-orders.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalledWith(loggerMock); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); + }); + + it('triggers updateAccountInfo', () => { + expect(mockUpdateAccountInfo).toHaveBeenCalledWith( + loggerMock, + [ + { + asset: 'USDT', + free: 50.0179958, + locked: 0 + } + ], + expect.any(String) + ); }); it('returns expected value', () => { @@ -567,6 +601,7 @@ describe('handle-open-orders.js', () => { { symbol: 'BTCUSDT', orderId: 46838, + origQty: '0.00001', price: '1799.58000000', stopPrice: '1800.1000', type: 'STOP_LOSS_LIMIT', @@ -575,7 +610,16 @@ describe('handle-open-orders.js', () => { ], buy: { limitPrice: 1800, openOrders: [] }, sell: { limitPrice: 1800, openOrders: [] }, - accountInfo: accountInfoJSON + symbolInfo: { + quoteAsset: 'USDT' + }, + quoteAssetBalance: { + free: 50, + locked: 0.0179958 + }, + accountInfo: { + account: 'updated' + } }); }); }); @@ -583,20 +627,14 @@ describe('handle-open-orders.js', () => { describe('when stop price is less than current limit price', () => { beforeEach(async () => { - const { binance, logger } = require('../../../../helpers'); - binanceMock = binance; - loggerMock = logger; - binanceMock.client.cancelOrder = jest.fn().mockResolvedValue(true); - - mockGetAccountInfoFromAPI = jest - .fn() - .mockResolvedValue(accountInfoJSON); + mockGetAccountInfo = jest.fn().mockResolvedValue(accountInfoJSON); mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - saveOverrideAction: mockSaveOverrideAction + getAccountInfo: mockGetAccountInfo, + updateAccountInfo: mockUpdateAccountInfo, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); step = require('../handle-open-orders'); @@ -631,10 +669,13 @@ describe('handle-open-orders.js', () => { sell: { limitPrice: 1800, openOrders: [] + }, + symbolInfo: { + quoteAsset: 'USDT' } }; - result = await step.execute(logger, rawData); + result = await step.execute(loggerMock, rawData); }); it('does not trigger cancelOrder', () => { @@ -645,8 +686,8 @@ describe('handle-open-orders.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('returns expected value', () => { @@ -677,7 +718,10 @@ describe('handle-open-orders.js', () => { } ] }, - sell: { limitPrice: 1800, openOrders: [] } + sell: { limitPrice: 1800, openOrders: [] }, + symbolInfo: { + quoteAsset: 'USDT' + } }); }); }); @@ -686,75 +730,53 @@ describe('handle-open-orders.js', () => { describe('when order is sell', () => { describe('when stop price is less or equal than current limit price', () => { describe('when cancel order is failed', () => { - beforeEach(async () => { - const { binance, logger, slack } = require('../../../../helpers'); - binanceMock = binance; - loggerMock = logger; - slackMock = slack; - - slack.sendMessage = jest.fn().mockResolvedValue(true); + beforeEach(() => { + slackMock.sendMessage = jest.fn().mockResolvedValue(true); binanceMock.client.cancelOrder = jest .fn() .mockRejectedValue(new Error('something happened')); - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([ - { - symbol: 'BTCUSDT', - orderId: 46840, - price: '1799.58000000', - stopPrice: '1800.1000', - type: 'STOP_LOSS_LIMIT', - side: 'SELL' - }, - { - symbol: 'BTCUSDT', - orderId: 46841, - price: '1799.58000000', - stopPrice: '1800.1000', - type: 'STOP_LOSS_LIMIT', - side: 'BUY' - } - ]); - - mockGetAccountInfoFromAPI = jest - .fn() - .mockResolvedValue(accountInfoJSON); + mockGetAccountInfo = jest.fn().mockResolvedValue(accountInfoJSON); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - saveOverrideAction: mockSaveOverrideAction + getAccountInfo: mockGetAccountInfo, + updateAccountInfo: mockUpdateAccountInfo, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); - - step = require('../handle-open-orders'); }); - beforeEach(async () => { - rawData = { - symbol: 'BTCUSDT', - isLocked: false, - featureToggle: { - notifyDebug: false - }, - action: 'not-determined', - openOrders: [ + describe('when got getAndCacheOpenOrdersForSymbol successfully', () => { + beforeEach(async () => { + mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([ { symbol: 'BTCUSDT', - orderId: 46838, + orderId: 46840, price: '1799.58000000', stopPrice: '1800.1000', type: 'STOP_LOSS_LIMIT', side: 'SELL' + }, + { + symbol: 'BTCUSDT', + orderId: 46841, + price: '1799.58000000', + stopPrice: '1800.1000', + type: 'STOP_LOSS_LIMIT', + side: 'BUY' } - ], - buy: { - limitPrice: 1800, - openOrders: [] - }, - sell: { - limitPrice: 1801, + ]); + + step = require('../handle-open-orders'); + + rawData = { + symbol: 'BTCUSDT', + isLocked: false, + featureToggle: { + notifyDebug: false + }, + action: 'not-determined', openOrders: [ { symbol: 'BTCUSDT', @@ -764,61 +786,62 @@ describe('handle-open-orders.js', () => { type: 'STOP_LOSS_LIMIT', side: 'SELL' } - ] - } - }; + ], + buy: { + limitPrice: 1800, + openOrders: [] + }, + sell: { + limitPrice: 1801, + openOrders: [ + { + symbol: 'BTCUSDT', + orderId: 46838, + price: '1799.58000000', + stopPrice: '1800.1000', + type: 'STOP_LOSS_LIMIT', + side: 'SELL' + } + ] + }, + symbolInfo: { + quoteAsset: 'USDT' + } + }; - result = await step.execute(loggerMock, rawData); - }); + result = await step.execute(loggerMock, rawData); + }); - it('triggers cancelOrder', () => { - expect(binanceMock.client.cancelOrder).toHaveBeenCalledWith({ - symbol: 'BTCUSDT', - orderId: 46838 + it('triggers cancelOrder', () => { + expect(binanceMock.client.cancelOrder).toHaveBeenCalledWith({ + symbol: 'BTCUSDT', + orderId: 46838 + }); }); - }); - it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); - }); + it('triggers getAndCacheOpenOrdersForSymbol', () => { + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); - }); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); + }); - it('does not trigger slack.sendMessage', () => { - expect(slackMock.sendMessage).not.toHaveBeenCalled(); - }); + it('does not trigger slack.sendMessage', () => { + expect(slackMock.sendMessage).not.toHaveBeenCalled(); + }); - it('returns expected value', () => { - expect(result).toStrictEqual({ - symbol: 'BTCUSDT', - isLocked: false, - featureToggle: { - notifyDebug: false - }, - action: 'sell-order-checking', - openOrders: [ - { - symbol: 'BTCUSDT', - orderId: 46840, - price: '1799.58000000', - stopPrice: '1800.1000', - type: 'STOP_LOSS_LIMIT', - side: 'SELL' + it('returns expected value', () => { + expect(result).toStrictEqual({ + symbol: 'BTCUSDT', + isLocked: false, + featureToggle: { + notifyDebug: false }, - { - symbol: 'BTCUSDT', - orderId: 46841, - price: '1799.58000000', - stopPrice: '1800.1000', - type: 'STOP_LOSS_LIMIT', - side: 'BUY' - } - ], - buy: { limitPrice: 1800, openOrders: [] }, - sell: { - limitPrice: 1801, + action: 'sell-order-checking', openOrders: [ { symbol: 'BTCUSDT', @@ -827,34 +850,52 @@ describe('handle-open-orders.js', () => { stopPrice: '1800.1000', type: 'STOP_LOSS_LIMIT', side: 'SELL' + }, + { + symbol: 'BTCUSDT', + orderId: 46841, + price: '1799.58000000', + stopPrice: '1800.1000', + type: 'STOP_LOSS_LIMIT', + side: 'BUY' } - ] - }, - accountInfo: accountInfoJSON + ], + buy: { limitPrice: 1800, openOrders: [] }, + sell: { + limitPrice: 1801, + openOrders: [ + { + symbol: 'BTCUSDT', + orderId: 46840, + price: '1799.58000000', + stopPrice: '1800.1000', + type: 'STOP_LOSS_LIMIT', + side: 'SELL' + } + ] + }, + symbolInfo: { + quoteAsset: 'USDT' + }, + accountInfo: accountInfoJSON + }); }); }); }); describe('when cancel order is succeed', () => { beforeEach(async () => { - const { binance, logger } = require('../../../../helpers'); - binanceMock = binance; - loggerMock = logger; - binanceMock.client.cancelOrder = jest.fn().mockResolvedValue(true); - mockGetAndCacheOpenOrdersForSymbol = jest .fn() .mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest - .fn() - .mockResolvedValue(accountInfoJSON); + mockGetAccountInfo = jest.fn().mockResolvedValue(accountInfoJSON); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - saveOverrideAction: mockSaveOverrideAction + getAccountInfo: mockGetAccountInfo, + updateAccountInfo: mockUpdateAccountInfo, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); step = require('../handle-open-orders'); @@ -867,6 +908,7 @@ describe('handle-open-orders.js', () => { { symbol: 'BTCUSDT', orderId: 46838, + origQty: '0.00001', price: '1799.58000000', stopPrice: '1800.1000', type: 'STOP_LOSS_LIMIT', @@ -883,16 +925,25 @@ describe('handle-open-orders.js', () => { { symbol: 'BTCUSDT', orderId: 46838, + origQty: '0.00001', price: '1799.58000000', stopPrice: '1800.1000', type: 'STOP_LOSS_LIMIT', side: 'SELL' } ] + }, + symbolInfo: { + baseAsset: 'BTC', + quoteAsset: 'USDT' + }, + baseAssetBalance: { + free: 0, + locked: 0.00001 } }; - result = await step.execute(logger, rawData); + result = await step.execute(loggerMock, rawData); }); it('triggers cancelOrder', () => { @@ -902,6 +953,28 @@ describe('handle-open-orders.js', () => { }); }); + it('does not trigger getAndCacheOpenOrdersForSymbol', () => { + expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); + }); + + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); + }); + + it('triggers updateAccountInfo', () => { + expect(mockUpdateAccountInfo).toHaveBeenCalledWith( + loggerMock, + [ + { + asset: 'BTC', + free: 0.00001, + locked: 0 + } + ], + expect.any(String) + ); + }); + it('returns expected value', () => { expect(result).toStrictEqual({ symbol: 'BTCUSDT', @@ -911,6 +984,7 @@ describe('handle-open-orders.js', () => { { symbol: 'BTCUSDT', orderId: 46838, + origQty: '0.00001', price: '1799.58000000', stopPrice: '1800.1000', type: 'STOP_LOSS_LIMIT', @@ -919,7 +993,17 @@ describe('handle-open-orders.js', () => { ], buy: { limitPrice: 1800, openOrders: [] }, sell: { limitPrice: 1801, openOrders: [] }, - accountInfo: accountInfoJSON + symbolInfo: { + baseAsset: 'BTC', + quoteAsset: 'USDT' + }, + baseAssetBalance: { + free: 0, + locked: 0.00001 + }, + accountInfo: { + account: 'updated' + } }); }); }); @@ -927,18 +1011,12 @@ describe('handle-open-orders.js', () => { describe('when stop price is more than current limit price', () => { beforeEach(async () => { - const { binance, logger } = require('../../../../helpers'); - binanceMock = binance; - loggerMock = logger; - binanceMock.client.cancelOrder = jest.fn().mockResolvedValue(true); - - mockGetAccountInfoFromAPI = jest - .fn() - .mockResolvedValue(accountInfoJSON); + mockGetAccountInfo = jest.fn().mockResolvedValue(accountInfoJSON); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - saveOverrideAction: mockSaveOverrideAction + getAccountInfo: mockGetAccountInfo, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); step = require('../handle-open-orders'); @@ -973,18 +1051,21 @@ describe('handle-open-orders.js', () => { side: 'SELL' } ] + }, + symbolInfo: { + quoteAsset: 'USDT' } }; - result = await step.execute(logger, rawData); + result = await step.execute(loggerMock, rawData); }); it('does not trigger cancelOrder', () => { expect(binanceMock.client.cancelOrder).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('returns expected value', () => { @@ -1015,6 +1096,9 @@ describe('handle-open-orders.js', () => { side: 'SELL' } ] + }, + symbolInfo: { + quoteAsset: 'USDT' } }); }); diff --git a/app/cronjob/trailingTrade/step/__tests__/place-buy-order.test.js b/app/cronjob/trailingTrade/step/__tests__/place-buy-order.test.js index 3c347d86..432a7946 100644 --- a/app/cronjob/trailingTrade/step/__tests__/place-buy-order.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/place-buy-order.test.js @@ -11,12 +11,12 @@ describe('place-buy-order.js', () => { let slackMock; let loggerMock; - let mockGetAndCacheOpenOrdersForSymbol; - let mockGetAccountInfoFromAPI; + let mockUpdateAccountInfo; let mockIsExceedAPILimit; let mockGetAPILimit; let mockSaveOrderStats; let mockSaveOverrideAction; + let mockGetAndCacheOpenOrdersForSymbol; let mockSaveGridTradeOrder; @@ -42,18 +42,19 @@ describe('place-buy-order.js', () => { mockSaveOrderStats = jest.fn().mockResolvedValue(true); mockSaveOverrideAction = jest.fn().mockResolvedValue(true); - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockUpdateAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); + mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); + jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + updateAccountInfo: mockUpdateAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, getAPILimit: mockGetAPILimit, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -103,7 +104,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 0 }, + quoteAssetBalance: { free: 0, locked: 0 }, buy: { currentPrice: 200, openOrders: [] }, tradingView: {}, overrideData: {} @@ -119,8 +120,8 @@ describe('place-buy-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger updateAccountInfo', () => { + expect(mockUpdateAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveGridTradeOrder', () => { @@ -272,7 +273,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -325,7 +326,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -370,7 +371,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -420,7 +421,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -473,7 +474,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -526,7 +527,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -577,7 +578,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -628,7 +629,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -683,7 +684,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -741,7 +742,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -790,13 +791,12 @@ describe('place-buy-order.js', () => { }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + updateAccountInfo: mockUpdateAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, getAPILimit: mockGetAPILimit, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); rawData = _.cloneDeep(orgRawData); @@ -830,18 +830,24 @@ describe('place-buy-order.js', () => { clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, currentGridTradeIndex: - t.rawData.symbolConfiguration.buy.currentGridTradeIndex, - nextCheck: expect.any(String) + t.rawData.symbolConfiguration.buy.currentGridTradeIndex } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + t.rawData.symbol + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers updateAccountInfo', () => { + expect(mockUpdateAccountInfo).toHaveBeenCalledWith( + loggerMock, + [{ asset: 'USDT', free: 52.472, locked: 48.528 }], + expect.any(String) + ); }); it('triggers saveOrderStats', () => { @@ -1035,7 +1041,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 9 }, + quoteAssetBalance: { free: 9, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -1095,7 +1101,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 0.00009 }, + quoteAssetBalance: { free: 0.00009, locked: 0 }, buy: { currentPrice: 0.044866, openOrders: [] @@ -1155,7 +1161,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 0.00009 }, + quoteAssetBalance: { free: 0.00009, locked: 0 }, buy: { currentPrice: 0.00003771, openOrders: [] @@ -1215,7 +1221,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 9 }, + quoteAssetBalance: { free: 9, locked: 0 }, buy: { currentPrice: 268748, openOrders: [] @@ -1296,7 +1302,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 10.01 }, + quoteAssetBalance: { free: 10.01, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -1358,7 +1364,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 0.0001 }, + quoteAssetBalance: { free: 0.0001, locked: 0 }, buy: { currentPrice: 0.044866, openOrders: [] @@ -1420,7 +1426,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 0.0001 }, + quoteAssetBalance: { free: 0.0001, locked: 0 }, buy: { currentPrice: 0.00003771, openOrders: [] @@ -1482,7 +1488,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 10.01 }, + quoteAssetBalance: { free: 10.01, locked: 0 }, buy: { currentPrice: 268748, openOrders: [] @@ -1568,12 +1574,12 @@ describe('place-buy-order.js', () => { mockIsExceedAPILimit = jest.fn().mockReturnValue(true); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + updateAccountInfo: mockUpdateAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, getAPILimit: mockGetAPILimit, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); const step = require('../place-buy-order'); @@ -1615,6 +1621,7 @@ describe('place-buy-order.js', () => { type: 'STOP_LOSS_LIMIT' } ]); + binanceMock.client.order = jest.fn().mockResolvedValue({ symbol: 'BTCUPUSDT', orderId: 2701762317, @@ -1624,13 +1631,12 @@ describe('place-buy-order.js', () => { }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + updateAccountInfo: mockUpdateAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, getAPILimit: mockGetAPILimit, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); const step = require('../place-buy-order'); @@ -1684,7 +1690,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 12 }, + quoteAssetBalance: { free: 12, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -1783,7 +1789,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 50 }, + quoteAssetBalance: { free: 50, locked: 10 }, buy: { currentPrice: 200, openOrders: [] @@ -1804,9 +1810,9 @@ describe('place-buy-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 }, + expectedBalances: [{ asset: 'USDT', free: 39.89, locked: 20.11 }], expected: { openOrders: [ { @@ -1910,7 +1916,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 0.002 }, + quoteAssetBalance: { free: 0.002, locked: 0.001 }, buy: { currentPrice: 0.044866, openOrders: [] @@ -1931,9 +1937,11 @@ describe('place-buy-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 }, + expectedBalances: [ + { asset: 'BTC', free: 0.001863923, locked: 0.001136077 } + ], expected: { openOrders: [ { @@ -2037,7 +2045,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 0.002 }, + quoteAssetBalance: { free: 0.002, locked: 0 }, buy: { currentPrice: 0.00003771, openOrders: [] @@ -2058,9 +2066,11 @@ describe('place-buy-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 }, + expectedBalances: [ + { asset: 'BTC', free: 0.00188564, locked: 0.00011436000000000001 } + ], expected: { openOrders: [ { @@ -2164,7 +2174,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 15 }, + quoteAssetBalance: { free: 15, locked: 0 }, buy: { currentPrice: 268748, openOrders: [] @@ -2185,9 +2195,15 @@ describe('place-buy-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 }, + expectedBalances: [ + { + asset: 'BRL', + free: 4.946952000000001, + locked: 10.053047999999999 + } + ], expected: { openOrders: [ { @@ -2291,7 +2307,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 100 }, + quoteAssetBalance: { free: 100, locked: 10 }, buy: { currentPrice: 289.48, openOrders: [] @@ -2312,9 +2328,11 @@ describe('place-buy-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 1, - nextCheck: expect.any(String) + currentGridTradeIndex: 1 }, + expectedBalances: [ + { asset: 'USDT', free: 89.9614, locked: 20.0386 } + ], expected: { openOrders: [ { @@ -2359,13 +2377,13 @@ describe('place-buy-order.js', () => { .mockResolvedValue(t.binanceMockClientOrderResult); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + updateAccountInfo: mockUpdateAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, getAPILimit: mockGetAPILimit, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: + mockGetAndCacheOpenOrdersForSymbol })); const step = require('../place-buy-order'); @@ -2390,11 +2408,18 @@ describe('place-buy-order.js', () => { }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + t.symbol + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers updateAccountInfo', () => { + expect(mockUpdateAccountInfo).toHaveBeenCalledWith( + loggerMock, + t.expectedBalances, + expect.any(String) + ); }); it('triggers saveOrderStats', () => { @@ -2415,7 +2440,7 @@ describe('place-buy-order.js', () => { [ { symbol: 'BTCUPUSDT', - mockGetAndCacheOpenOrdersForSymbol: [ + openOrders: [ { orderId: 123, price: 202.2, @@ -2483,7 +2508,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 101 }, + quoteAssetBalance: { free: 101, locked: 0 }, buy: { currentPrice: 200, openOrders: [] @@ -2504,9 +2529,15 @@ describe('place-buy-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 }, + expectedBalances: [ + { + asset: 'USDT', + free: 52.472, + locked: 48.528 + } + ], expected: { openOrders: [ { @@ -2542,7 +2573,7 @@ describe('place-buy-order.js', () => { }, { symbol: 'ETHBTC', - mockGetAndCacheOpenOrdersForSymbol: [ + openOrders: [ { orderId: 456, price: 0.045359, @@ -2610,7 +2641,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 0.002 }, + quoteAssetBalance: { free: 0.002, locked: 0 }, buy: { currentPrice: 0.044866, openOrders: [] @@ -2631,9 +2662,15 @@ describe('place-buy-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 }, + expectedBalances: [ + { + asset: 'BTC', + free: 0.0010021020000000002, + locked: 0.0009978979999999999 + } + ], expected: { openOrders: [ { @@ -2669,7 +2706,7 @@ describe('place-buy-order.js', () => { }, { symbol: 'ALPHABTC', - mockGetAndCacheOpenOrdersForSymbol: [ + openOrders: [ { orderId: 456, price: 0.00003812, @@ -2737,7 +2774,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 0.002 }, + quoteAssetBalance: { free: 0.002, locked: 0 }, buy: { currentPrice: 0.00003771, openOrders: [] @@ -2758,9 +2795,15 @@ describe('place-buy-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 }, + expectedBalances: [ + { + asset: 'BTC', + free: 0.00100888, + locked: 0.00099112 + } + ], expected: { openOrders: [ { @@ -2796,7 +2839,7 @@ describe('place-buy-order.js', () => { }, { symbol: 'BTCBRL', - mockGetAndCacheOpenOrdersForSymbol: [ + openOrders: [ { orderId: 456, price: 271704, @@ -2864,7 +2907,7 @@ describe('place-buy-order.js', () => { } }, action: 'buy', - quoteAssetBalance: { free: 11 }, + quoteAssetBalance: { free: 11, locked: 0 }, buy: { currentPrice: 268748, openOrders: [] @@ -2885,9 +2928,15 @@ describe('place-buy-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 }, + expectedBalances: [ + { + asset: 'BRL', + free: 0.13183999999999862, + locked: 10.868160000000001 + } + ], expected: { openOrders: [ { @@ -2926,20 +2975,20 @@ describe('place-buy-order.js', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest .fn() - .mockResolvedValue(t.mockGetAndCacheOpenOrdersForSymbol); + .mockResolvedValue(t.openOrders); binanceMock.client.order = jest .fn() .mockResolvedValue(t.binanceMockClientOrderResult); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + updateAccountInfo: mockUpdateAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, getAPILimit: mockGetAPILimit, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: + mockGetAndCacheOpenOrdersForSymbol })); const step = require('../place-buy-order'); @@ -2964,11 +3013,18 @@ describe('place-buy-order.js', () => { }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + t.symbol + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers updateAccountInfo', () => { + expect(mockUpdateAccountInfo).toHaveBeenCalledWith( + loggerMock, + t.expectedBalances, + expect.any(String) + ); }); it('triggers saveOrderStats', () => { @@ -2984,6 +3040,154 @@ describe('place-buy-order.js', () => { }); }); }); + + describe('when order is placed, but cache is not returning open orders due to a cache error', () => { + beforeEach(async () => { + mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); + + binanceMock.client.order = jest.fn().mockResolvedValue({ + symbol: 'BTCUPUSDT', + orderId: 2701762317, + orderListId: -1, + clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', + transactTime: 1626946722520 + }); + + jest.mock('../../../trailingTradeHelper/common', () => ({ + updateAccountInfo: mockUpdateAccountInfo, + isExceedAPILimit: mockIsExceedAPILimit, + getAPILimit: mockGetAPILimit, + saveOrderStats: mockSaveOrderStats, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol + })); + + const step = require('../place-buy-order'); + + rawData = _.cloneDeep({ + symbol: 'BTCUPUSDT', + isLocked: false, + featureToggle: { + notifyDebug: true + }, + symbolInfo: { + baseAsset: 'BTCUP', + quoteAsset: 'USDT', + filterLotSize: { stepSize: '0.01000000', minQty: '0.01000000' }, + filterPrice: { tickSize: '0.00100000' }, + filterMinNotional: { minNotional: '10.00000000' } + }, + symbolConfiguration: { + symbols: ['BTCUPUSDT', 'ETHBTC', 'ALPHABTC', 'BTCBRL', 'BNBUSDT'], + buy: { + enabled: true, + currentGridTradeIndex: 0, + currentGridTrade: { + triggerPercentage: 1, + minPurchaseAmount: 10, + maxPurchaseAmount: 50, + stopPercentage: 1.01, + limitPercentage: 1.011, + executed: false, + executedOrder: null + }, + tradingView: { + whenStrongBuy: false, + whenBuy: false + } + }, + botOptions: { + tradingView: { + useOnlyWithin: 5, + ifExpires: 'ignore' + } + }, + system: { + checkOrderExecutePeriod: 10 + } + }, + action: 'buy', + quoteAssetBalance: { free: 101, locked: 0 }, + buy: { + currentPrice: 200, + openOrders: [] + } + }); + + result = await step.execute(loggerMock, rawData); + }); + + it('triggers binance.client.order', () => { + expect(binanceMock.client.order).toHaveBeenCalledWith({ + price: 202.2, + quantity: 0.24, + side: 'buy', + stopPrice: 202, + symbol: 'BTCUPUSDT', + timeInForce: 'GTC', + type: 'STOP_LOSS_LIMIT' + }); + }); + + it('triggers saveGridTradeOrder for grid trade last buy order', () => { + expect(mockSaveGridTradeOrder).toHaveBeenCalledWith( + loggerMock, + `BTCUPUSDT-grid-trade-last-buy-order`, + { + symbol: 'BTCUPUSDT', + orderId: 2701762317, + orderListId: -1, + clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', + transactTime: 1626946722520, + currentGridTradeIndex: 0 + } + ); + }); + + it('triggers getAndCacheOpenOrdersForSymbol', () => { + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); + }); + + it('triggers updateAccountInfo', () => { + expect(mockUpdateAccountInfo).toHaveBeenCalledWith( + loggerMock, + [ + { + asset: 'USDT', + free: 52.472, + locked: 48.528 + } + ], + expect.any(String) + ); + }); + + it('triggers saveOrderStats', () => { + expect(mockSaveOrderStats).toHaveBeenCalledWith(loggerMock, [ + 'BTCUPUSDT', + 'ETHBTC', + 'ALPHABTC', + 'BTCBRL', + 'BNBUSDT' + ]); + }); + + it('retruns expected value', () => { + expect(result).toMatchObject({ + openOrders: [], + buy: { + currentPrice: 200, + openOrders: [], + processMessage: + 'Placed new stop loss limit order for buying of grid trade #1.', + updatedAt: expect.any(Object) + } + }); + }); + }); }); }); }); diff --git a/app/cronjob/trailingTrade/step/__tests__/place-manual-trade.test.js b/app/cronjob/trailingTrade/step/__tests__/place-manual-trade.test.js index 1a530387..ff3b08ae 100644 --- a/app/cronjob/trailingTrade/step/__tests__/place-manual-trade.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/place-manual-trade.test.js @@ -7,11 +7,10 @@ describe('place-manual-trade.js', () => { let binanceMock; let slackMock; let loggerMock; - let cacheMock; - let mockGetAndCacheOpenOrdersForSymbol; - let mockGetAccountInfoFromAPI; + let mockGetAccountInfo; let mockGetAPILimit; + let mockGetAndCacheOpenOrdersForSymbol; let mockSaveManualOrder; @@ -24,21 +23,21 @@ describe('place-manual-trade.js', () => { () => () => jest.requireActual('moment')('2020-01-01T00:00:00.000Z') ); - const { binance, slack, cache, logger } = require('../../../../helpers'); + const { binance, slack, logger } = require('../../../../helpers'); binanceMock = binance; slackMock = slack; loggerMock = logger; - cacheMock = cache; - cacheMock.hset = jest.fn().mockResolvedValue(true); slackMock.sendMessage = jest.fn().mockResolvedValue(true); binanceMock.client.order = jest.fn().mockResolvedValue(true); - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockReturnValue([]); - mockGetAccountInfoFromAPI = jest - .fn() - .mockResolvedValue({ account: 'info' }); + mockGetAccountInfo = jest.fn().mockResolvedValue({ + account: 'info' + }); + + mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); + mockGetAPILimit = jest.fn().mockResolvedValue(10); mockSaveManualOrder = jest.fn().mockResolvedValue(true); }); @@ -46,9 +45,9 @@ describe('place-manual-trade.js', () => { describe('when symbol is locked', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAPILimit: mockGetAPILimit + getAccountInfo: mockGetAccountInfo, + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -72,8 +71,8 @@ describe('place-manual-trade.js', () => { result = await step.execute(loggerMock, rawData); }); - it('does not trigger cache.hset', () => { - expect(cacheMock.hset).not.toHaveBeenCalled(); + it('does not trigger getAndCacheOpenOrdersForSymbol', () => { + expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); it('does not trigger saveManualOrder', () => { @@ -98,9 +97,9 @@ describe('place-manual-trade.js', () => { describe('when action is not manual-trade', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAPILimit: mockGetAPILimit + getAccountInfo: mockGetAccountInfo, + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -124,8 +123,8 @@ describe('place-manual-trade.js', () => { result = await step.execute(loggerMock, rawData); }); - it('does not trigger cache.hset', () => { - expect(cacheMock.hset).not.toHaveBeenCalled(); + it('does not trigger getAndCacheOpenOrdersForSymbol', () => { + expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); it('does not trigger saveManualOrder', () => { @@ -722,7 +721,7 @@ describe('place-manual-trade.js', () => { } ] }, - openOrders: [], + openOrders: null, expectedData: { symbol: 'BTCUSDT', action: 'manual-trade', @@ -766,12 +765,14 @@ describe('place-manual-trade.js', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest .fn() - .mockResolvedValue(testData.openOrders); + .mockResolvedValue( + testData.openOrders !== null ? testData.openOrders : [] + ); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAPILimit: mockGetAPILimit + getAccountInfo: mockGetAccountInfo, + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -817,8 +818,7 @@ describe('place-manual-trade.js', () => { 'BTCUSDT', testData.orderResult.orderId, { - ...testData.orderResult, - nextCheck: expect.any(String) + ...testData.orderResult } ); }); @@ -831,12 +831,10 @@ describe('place-manual-trade.js', () => { describe('when unknown order side/type is provided', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, - getAPILimit: mockGetAPILimit + getAccountInfo: mockGetAccountInfo, + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ diff --git a/app/cronjob/trailingTrade/step/__tests__/place-sell-order.test.js b/app/cronjob/trailingTrade/step/__tests__/place-sell-order.test.js index 8bc4a756..ae748cc3 100644 --- a/app/cronjob/trailingTrade/step/__tests__/place-sell-order.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/place-sell-order.test.js @@ -8,10 +8,10 @@ describe('place-sell-order.js', () => { let slackMock; let loggerMock; - let mockGetAndCacheOpenOrdersForSymbol; - let mockGetAccountInfoFromAPI; + let mockGetAccountInfo; let mockIsExceedAPILimit; let mockGetAPILimit; + let mockGetAndCacheOpenOrdersForSymbol; let mockSaveGridTradeOrder; @@ -34,20 +34,21 @@ describe('place-sell-order.js', () => { mockGetAPILimit = jest.fn().mockResolvedValue(10); mockSaveGridTradeOrder = jest.fn().mockResolvedValue(true); + + mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); }); describe('when symbol is locked', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -101,8 +102,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -112,16 +113,15 @@ describe('place-sell-order.js', () => { describe('when action is not sell', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -175,8 +175,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -186,16 +186,15 @@ describe('place-sell-order.js', () => { describe('when open orders exist', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -261,8 +260,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -292,16 +291,15 @@ describe('place-sell-order.js', () => { describe('when current grid trade is null', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -351,8 +349,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -374,16 +372,15 @@ describe('place-sell-order.js', () => { describe('when quantity is not enough', () => { describe('BTCUPUSDT', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -440,8 +437,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveGridTradeOrder', () => { @@ -467,16 +464,15 @@ describe('place-sell-order.js', () => { describe('ALPHABTC', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -533,8 +529,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveGridTradeOrder', () => { @@ -560,16 +556,15 @@ describe('place-sell-order.js', () => { describe('BTCBRL', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -626,8 +621,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveGridTradeOrder', () => { @@ -655,16 +650,15 @@ describe('place-sell-order.js', () => { describe('when order amount is less than minimum notional', () => { describe('BTCUPUSDT', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -721,8 +715,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveGridTradeOrder', () => { @@ -748,16 +742,15 @@ describe('place-sell-order.js', () => { describe('ALPHBTC', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -814,8 +807,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveGridTradeOrder', () => { @@ -841,16 +834,15 @@ describe('place-sell-order.js', () => { describe('BTCBRL', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -907,8 +899,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveGridTradeOrder', () => { @@ -935,16 +927,15 @@ describe('place-sell-order.js', () => { describe('when trading is disabled', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -1001,8 +992,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveGridTradeOrder', () => { @@ -1029,16 +1020,15 @@ describe('place-sell-order.js', () => { beforeEach(async () => { mockIsExceedAPILimit = jest.fn().mockReturnValue(true); - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -1095,8 +1085,8 @@ describe('place-sell-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveGridTradeOrder', () => { @@ -1143,16 +1133,15 @@ describe('place-sell-order.js', () => { transactTime: 1626946722520 }); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -1223,18 +1212,20 @@ describe('place-sell-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -1298,16 +1289,15 @@ describe('place-sell-order.js', () => { transactTime: 1626946722520 }); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -1378,18 +1368,20 @@ describe('place-sell-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'ALPHABTC' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -1453,16 +1445,15 @@ describe('place-sell-order.js', () => { transactTime: 1626946722520 }); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -1533,18 +1524,20 @@ describe('place-sell-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCBRL' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -1611,16 +1604,16 @@ describe('place-sell-order.js', () => { transactTime: 1626946722520 }); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: + mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -1691,18 +1684,20 @@ describe('place-sell-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -1766,16 +1761,16 @@ describe('place-sell-order.js', () => { transactTime: 1626946722520 }); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: + mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -1846,18 +1841,20 @@ describe('place-sell-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'ALPHABTC' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -1921,16 +1918,16 @@ describe('place-sell-order.js', () => { transactTime: 1626946722520 }); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: + mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -2001,18 +1998,20 @@ describe('place-sell-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCBRL' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -2078,16 +2077,16 @@ describe('place-sell-order.js', () => { transactTime: 1626946722520 }); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: + mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -2158,18 +2157,20 @@ describe('place-sell-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -2233,16 +2234,16 @@ describe('place-sell-order.js', () => { transactTime: 1626946722520 }); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: + mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -2313,18 +2314,20 @@ describe('place-sell-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'ALPHABTC' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -2388,16 +2391,16 @@ describe('place-sell-order.js', () => { transactTime: 1626946722520 }); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: + mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -2468,18 +2471,20 @@ describe('place-sell-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCBRL' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('retruns expected value', () => { @@ -2543,16 +2548,16 @@ describe('place-sell-order.js', () => { transactTime: 1626946722520 }); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: + mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -2623,18 +2628,20 @@ describe('place-sell-order.js', () => { orderListId: -1, clientOrderId: '6eGYHaJbmJrIS40eoq8ziM', transactTime: 1626946722520, - currentGridTradeIndex: 0, - nextCheck: expect.any(String) + currentGridTradeIndex: 0 } ); }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'ONGUSDT' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('retruns expected value', () => { diff --git a/app/cronjob/trailingTrade/step/__tests__/place-sell-stop-loss-order.test.js b/app/cronjob/trailingTrade/step/__tests__/place-sell-stop-loss-order.test.js index 4ecdf120..030ca77d 100644 --- a/app/cronjob/trailingTrade/step/__tests__/place-sell-stop-loss-order.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/place-sell-stop-loss-order.test.js @@ -8,11 +8,11 @@ describe('place-sell-stop-loss-order.js', () => { let slackMock; let loggerMock; - let mockGetAndCacheOpenOrdersForSymbol; - let mockGetAccountInfoFromAPI; + let mockGetAccountInfo; let mockIsExceedAPILimit; let mockDisableAction; let mockGetAPILimit; + let mockGetAndCacheOpenOrdersForSymbol; let mockSaveSymbolGridTrade; @@ -35,21 +35,23 @@ describe('place-sell-stop-loss-order.js', () => { mockGetAPILimit = jest.fn().mockReturnValueOnce(10); mockSaveSymbolGridTrade = jest.fn().mockResolvedValue(true); + + mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); }); describe('when symbol is locked', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -98,8 +100,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -114,16 +116,16 @@ describe('place-sell-stop-loss-order.js', () => { describe('when action is not sell-stop-loss', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -172,8 +174,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -188,16 +190,16 @@ describe('place-sell-stop-loss-order.js', () => { describe('when open orders exist', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -258,8 +260,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -295,16 +297,16 @@ describe('place-sell-stop-loss-order.js', () => { describe('BTCUPUSDT', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -356,8 +358,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -384,16 +386,16 @@ describe('place-sell-stop-loss-order.js', () => { describe('ALPHABTC', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -445,8 +447,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -473,16 +475,16 @@ describe('place-sell-stop-loss-order.js', () => { describe('BTCBRL', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -534,8 +536,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -564,16 +566,16 @@ describe('place-sell-stop-loss-order.js', () => { describe('BTCUPUSDT', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -625,8 +627,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -652,16 +654,16 @@ describe('place-sell-stop-loss-order.js', () => { describe('ALPHABTC', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -713,8 +715,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -740,16 +742,16 @@ describe('place-sell-stop-loss-order.js', () => { describe('BTCBRL', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -801,8 +803,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -829,16 +831,16 @@ describe('place-sell-stop-loss-order.js', () => { describe('when trading is disabled', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -890,8 +892,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -919,16 +921,16 @@ describe('place-sell-stop-loss-order.js', () => { mockIsExceedAPILimit = jest.fn().mockReturnValue(true); mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -980,8 +982,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -1007,16 +1009,16 @@ describe('place-sell-stop-loss-order.js', () => { describe('when stop loss order type is not market', () => { beforeEach(async () => { mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -1068,8 +1070,8 @@ describe('place-sell-stop-loss-order.js', () => { expect(mockGetAndCacheOpenOrdersForSymbol).not.toHaveBeenCalled(); }); - it('does not trigger getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).not.toHaveBeenCalled(); + it('does not trigger getAccountInfo', () => { + expect(mockGetAccountInfo).not.toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -1105,7 +1107,7 @@ describe('place-sell-stop-loss-order.js', () => { type: 'MARKET' } ]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); @@ -1118,12 +1120,11 @@ describe('place-sell-stop-loss-order.js', () => { }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -1210,11 +1211,14 @@ describe('place-sell-stop-loss-order.js', () => { }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('triggers saveSymbolGridTrade', () => { @@ -1295,7 +1299,7 @@ describe('place-sell-stop-loss-order.js', () => { type: 'MARKET' } ]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); @@ -1308,12 +1312,11 @@ describe('place-sell-stop-loss-order.js', () => { }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -1407,11 +1410,14 @@ describe('place-sell-stop-loss-order.js', () => { }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'ALPHABTC' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('triggers saveSymbolGridTrade', () => { @@ -1499,7 +1505,7 @@ describe('place-sell-stop-loss-order.js', () => { type: 'MARKET' } ]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); @@ -1512,12 +1518,11 @@ describe('place-sell-stop-loss-order.js', () => { }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -1611,11 +1616,14 @@ describe('place-sell-stop-loss-order.js', () => { }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCBRL' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('triggers saveSymbolGridTrade', () => { @@ -1705,7 +1713,7 @@ describe('place-sell-stop-loss-order.js', () => { type: 'MARKET' } ]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); @@ -1718,12 +1726,11 @@ describe('place-sell-stop-loss-order.js', () => { }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -1807,11 +1814,14 @@ describe('place-sell-stop-loss-order.js', () => { }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('does not trigger saveSymbolGridTrade', () => { @@ -1899,7 +1909,7 @@ describe('place-sell-stop-loss-order.js', () => { type: 'MARKET' } ]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); @@ -1912,12 +1922,11 @@ describe('place-sell-stop-loss-order.js', () => { }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -2001,11 +2010,14 @@ describe('place-sell-stop-loss-order.js', () => { }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'ALPHABTC' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('triggers saveSymbolGridTrade', () => { @@ -2093,7 +2105,7 @@ describe('place-sell-stop-loss-order.js', () => { type: 'MARKET' } ]); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info' }); @@ -2106,12 +2118,11 @@ describe('place-sell-stop-loss-order.js', () => { }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI: mockGetAccountInfoFromAPI, + getAccountInfo: mockGetAccountInfo, isExceedAPILimit: mockIsExceedAPILimit, disableAction: mockDisableAction, - getAPILimit: mockGetAPILimit + getAPILimit: mockGetAPILimit, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -2195,11 +2206,14 @@ describe('place-sell-stop-loss-order.js', () => { }); it('triggers getAndCacheOpenOrdersForSymbol', () => { - expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalled(); + expect(mockGetAndCacheOpenOrdersForSymbol).toHaveBeenCalledWith( + loggerMock, + 'BTCBRL' + ); }); - it('triggers getAccountInfoFromAPI', () => { - expect(mockGetAccountInfoFromAPI).toHaveBeenCalled(); + it('triggers getAccountInfo', () => { + expect(mockGetAccountInfo).toHaveBeenCalled(); }); it('triggers saveSymbolGridTrade', () => { diff --git a/app/cronjob/trailingTrade/step/__tests__/remove-last-buy-price.test.js b/app/cronjob/trailingTrade/step/__tests__/remove-last-buy-price.test.js index 26226ba5..18c7f1b0 100644 --- a/app/cronjob/trailingTrade/step/__tests__/remove-last-buy-price.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/remove-last-buy-price.test.js @@ -8,12 +8,12 @@ describe('remove-last-buy-price.js', () => { let slackMock; let loggerMock; - let mockGetAndCacheOpenOrdersForSymbol; let mockGetAPILimit; let mockIsActionDisabled; let mockRemoveLastBuyPrice; let mockSaveOrderStats; let mockSaveOverrideAction; + let mockGetAndCacheOpenOrdersForSymbol; let mockArchiveSymbolGridTrade; let mockDeleteSymbolGridTrade; @@ -59,12 +59,12 @@ describe('remove-last-buy-price.js', () => { describe('when symbol is locked', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -129,12 +129,12 @@ describe('remove-last-buy-price.js', () => { describe('when action is not `not-determined`', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -199,12 +199,12 @@ describe('remove-last-buy-price.js', () => { describe('when grid trade last buy order exists', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -276,12 +276,12 @@ describe('remove-last-buy-price.js', () => { describe('when grid trade last sell order exists', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -353,12 +353,12 @@ describe('remove-last-buy-price.js', () => { describe('when last buy price is not set', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -425,12 +425,12 @@ describe('remove-last-buy-price.js', () => { describe('when open orders exist', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -513,12 +513,12 @@ describe('remove-last-buy-price.js', () => { }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -592,12 +592,12 @@ describe('remove-last-buy-price.js', () => { ]); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -664,12 +664,12 @@ describe('remove-last-buy-price.js', () => { describe('when cannot find open orders', () => { beforeEach(() => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); @@ -852,12 +852,12 @@ describe('remove-last-buy-price.js', () => { ]); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); jest.mock('../../../trailingTradeHelper/configuration', () => ({ @@ -925,13 +925,12 @@ describe('remove-last-buy-price.js', () => { describe('last buy price remove threshold is same as minimum notional', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); mockArchiveSymbolGridTrade = jest.fn().mockResolvedValue({ @@ -1054,13 +1053,12 @@ describe('remove-last-buy-price.js', () => { describe('last buy price remove threshold is less than minimum notional', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: - mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); mockArchiveSymbolGridTrade = jest.fn().mockResolvedValue({ @@ -1150,12 +1148,12 @@ describe('remove-last-buy-price.js', () => { describe('when there is enough balance', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol, isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, removeLastBuyPrice: mockRemoveLastBuyPrice, saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); mockArchiveSymbolGridTrade = jest.fn().mockResolvedValue({ diff --git a/app/cronjob/trailingTrade/step/__tests__/save-data-to-cache.test.js b/app/cronjob/trailingTrade/step/__tests__/save-data-to-cache.test.js index f3788969..2f39e001 100644 --- a/app/cronjob/trailingTrade/step/__tests__/save-data-to-cache.test.js +++ b/app/cronjob/trailingTrade/step/__tests__/save-data-to-cache.test.js @@ -1,4 +1,4 @@ -const { cache, logger } = require('../../../../helpers'); +const { logger, mongo } = require('../../../../helpers'); const step = require('../save-data-to-cache'); @@ -9,7 +9,7 @@ describe('save-data-to-cache.js', () => { describe('execute', () => { describe('when save to cache is disabled', () => { beforeEach(async () => { - cache.hset = jest.fn().mockResolvedValue(true); + mongo.upsertOne = jest.fn().mockResolvedValue(true); rawData = { symbol: 'BTCUSDT', @@ -19,8 +19,8 @@ describe('save-data-to-cache.js', () => { result = await step.execute(logger, rawData); }); - it('does not trigger cache.hset', () => { - expect(cache.hset).not.toHaveBeenCalled(); + it('does not trigger mongo.upsertOne', () => { + expect(mongo.upsertOne).not.toHaveBeenCalled(); }); it('returns expected value', () => { @@ -33,29 +33,50 @@ describe('save-data-to-cache.js', () => { describe('when save to cache is enabled', () => { beforeEach(async () => { - cache.hset = jest.fn().mockResolvedValue(true); + mongo.upsertOne = jest.fn().mockResolvedValue(true); rawData = { symbol: 'BTCUSDT', - saveToCache: true + saveToCache: true, + closedTrades: 'something', + accountInfo: { some: 'thing' }, + symbolConfiguration: { + candles: { + interval: '1m' + }, + symbols: ['BTCUSDT', 'ETHUSDT'] + }, + other: 'data', + tradingView: { + some: 'thing' + } }; result = await step.execute(logger, rawData); }); - it('triggers cache.hset', () => { - expect(cache.hset).toHaveBeenCalledWith( - 'trailing-trade-symbols', - 'BTCUSDT-processed-data', - JSON.stringify(rawData) + it('triggers mongo.upsertOne', () => { + expect(mongo.upsertOne).toHaveBeenCalledWith( + logger, + 'trailing-trade-cache', + { + symbol: 'BTCUSDT' + }, + { + other: 'data', + saveToCache: true, + symbol: 'BTCUSDT', + symbolConfiguration: { + candles: { + interval: '1m' + } + } + } ); }); it('returns expected value', () => { - expect(result).toStrictEqual({ - symbol: 'BTCUSDT', - saveToCache: true - }); + expect(result).toStrictEqual(rawData); }); }); }); diff --git a/app/cronjob/trailingTrade/step/cancel-order.js b/app/cronjob/trailingTrade/step/cancel-order.js index 599e97a9..51b23367 100644 --- a/app/cronjob/trailingTrade/step/cancel-order.js +++ b/app/cronjob/trailingTrade/step/cancel-order.js @@ -2,8 +2,8 @@ const moment = require('moment'); const { binance, slack, PubSub } = require('../../../helpers'); const { getAPILimit, - getAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI + getAccountInfo, + getAndCacheOpenOrdersForSymbol } = require('../../trailingTradeHelper/common'); const { deleteManualOrder } = require('../../trailingTradeHelper/order'); @@ -62,7 +62,7 @@ const execute = async (logger, rawData) => { ); // Refresh account info - data.accountInfo = await getAccountInfoFromAPI(logger); + data.accountInfo = await getAccountInfo(logger); PubSub.publish('frontend-notification', { type: 'success', @@ -82,7 +82,7 @@ const execute = async (logger, rawData) => { ); data.buy.processMessage = `The order has been cancelled.`; - data.buy.updatedAt = moment().utc(); + data.buy.updatedAt = moment().utc().toDate(); return data; }; diff --git a/app/cronjob/trailingTrade/step/determine-action.js b/app/cronjob/trailingTrade/step/determine-action.js index 2be1ca58..80a0d33f 100644 --- a/app/cronjob/trailingTrade/step/determine-action.js +++ b/app/cronjob/trailingTrade/step/determine-action.js @@ -175,7 +175,7 @@ const setBuyActionAndMessage = (logger, rawData, action, processMessage) => { data.action = action; data.buy.processMessage = processMessage; - data.buy.updatedAt = moment().utc(); + data.buy.updatedAt = moment().utc().toDate(); logger.info({ data, saveLog: true }, processMessage); return data; @@ -414,7 +414,7 @@ const setSellActionAndMessage = (logger, rawData, action, processMessage) => { data.action = action; data.sell.processMessage = processMessage; - data.sell.updatedAt = moment().utc(); + data.sell.updatedAt = moment().utc().toDate(); logger.info({ data, saveLog: true }, processMessage); return data; diff --git a/app/cronjob/trailingTrade/step/ensure-grid-trade-order-executed.js b/app/cronjob/trailingTrade/step/ensure-grid-trade-order-executed.js index e9c73a65..baaba3d5 100644 --- a/app/cronjob/trailingTrade/step/ensure-grid-trade-order-executed.js +++ b/app/cronjob/trailingTrade/step/ensure-grid-trade-order-executed.js @@ -1,7 +1,7 @@ const moment = require('moment'); const _ = require('lodash'); -const { slack, PubSub, binance } = require('../../../helpers'); +const { slack, PubSub } = require('../../../helpers'); const { calculateLastBuyPrice, getAPILimit, @@ -14,52 +14,10 @@ const { saveSymbolGridTrade } = require('../../trailingTradeHelper/configuration'); const { - getGridTradeOrder, deleteGridTradeOrder, - saveGridTradeOrder + getGridTradeLastOrder } = require('../../trailingTradeHelper/order'); -/** - * Retrieve last grid order from cache - * - * @param {*} logger - * @param {*} symbol - * @param {*} side - * @returns - */ -const getGridTradeLastOrder = async (logger, symbol, side) => { - const lastOrder = - (await getGridTradeOrder( - logger, - `${symbol}-grid-trade-last-${side}-order` - )) || {}; - - logger.info( - { lastOrder }, - `Retrieved grid trade last ${side} order from cache` - ); - - return lastOrder; -}; - -/** - * Update grid trade order - * - * @param {*} logger - * @param {*} symbol - * @param {*} side - * @param {*} newOrder - */ -const updateGridTradeLastOrder = async (logger, symbol, side, newOrder) => { - await saveGridTradeOrder( - logger, - `${symbol}-grid-trade-last-${side}-order`, - newOrder - ); - - logger.info(`Updated grid trade last ${side} order to cache`); -}; - /** * Remove last order from cache * @@ -81,30 +39,24 @@ const removeGridTradeLastOrder = async (logger, symbols, symbol, side) => { * @param {*} logger * @param {*} symbol * @param {*} side - * @param {*} orderParams - * @param {*} orderResult + * @param {*} order * @param {*} notifyOrderExecute */ const slackMessageOrderFilled = async ( logger, symbol, side, - orderParams, - orderResult, + order, notifyOrderExecute ) => { - const type = - orderParams?.type?.toUpperCase() || - orderResult?.type?.toUpperCase() || - 'Undefined'; + const type = order.type.toUpperCase(); - const humanisedGridTradeIndex = orderParams.currentGridTradeIndex + 1; + const humanisedGridTradeIndex = order.currentGridTradeIndex + 1; logger.info( { side, - orderParams, - orderResult, + order, saveLog: true }, `The ${side} order of the grid trade #${humanisedGridTradeIndex} ` + @@ -123,11 +75,7 @@ const slackMessageOrderFilled = async ( `${symbol} ${side.toUpperCase()} Grid Trade #${humanisedGridTradeIndex} Order Filled (${moment().format( 'HH:mm:ss.SSS' )}): *${type}*\n` + - `- Order Result: \`\`\`${JSON.stringify( - orderResult, - undefined, - 2 - )}\`\`\`\n` + + `- Order Result: \`\`\`${JSON.stringify(order, undefined, 2)}\`\`\`\n` + `- Current API Usage: ${getAPILimit(logger)}` ); } @@ -141,41 +89,35 @@ const slackMessageOrderFilled = async ( * @param {*} logger * @param {*} symbol * @param {*} side - * @param {*} orderParams - * @param {*} orderResult + * @param {*} order * @param {*} notifyOrderExecute */ const slackMessageOrderDeleted = async ( logger, symbol, side, - orderParams, - orderResult, + order, notifyOrderExecute ) => { - const type = - orderParams?.type?.toUpperCase() || - orderResult?.type?.toUpperCase() || - 'Undefined'; + const type = order.type.toUpperCase(); - const humanisedGridTradeIndex = orderParams.currentGridTradeIndex + 1; + const humanisedGridTradeIndex = order.currentGridTradeIndex + 1; logger.info( { side, - orderParams, - orderResult, + order, saveLog: true }, `The ${side} order of the grid trade #${humanisedGridTradeIndex} ` + - `for ${symbol} is ${orderResult.status}. Stop monitoring.` + `for ${symbol} is ${order.status}. Stop monitoring.` ); PubSub.publish('frontend-notification', { type: 'success', title: `The ${side} order of the grid trade #${humanisedGridTradeIndex} ` + - `for ${symbol} is ${orderResult.status}. Stop monitoring.` + `for ${symbol} is ${order.status}. Stop monitoring.` }); if (notifyOrderExecute) { @@ -183,11 +125,7 @@ const slackMessageOrderDeleted = async ( `${symbol} ${side.toUpperCase()} Grid Trade #${humanisedGridTradeIndex} Order Removed (${moment().format( 'HH:mm:ss.SSS' )}): *${type}*\n` + - `- Order Result: \`\`\`${JSON.stringify( - orderResult, - undefined, - 2 - )}\`\`\`\n` + + `- Order Result: \`\`\`${JSON.stringify(order, undefined, 2)}\`\`\`\n` + `- Current API Usage: ${getAPILimit(logger)}` ); } @@ -210,7 +148,7 @@ const saveGridTrade = async (logger, rawData, order) => { sell: { gridTrade: sellGridTrade } } } = rawData; - // Assummed grid trade in the symbol configuration is already up to date. + // Assumed grid trade in the symbol configuration is already up to date. const { side, type, currentGridTradeIndex } = order; const newGridTrade = { @@ -262,10 +200,7 @@ const execute = async (logger, rawData) => { featureToggle: { notifyOrderExecute }, symbolConfiguration: { symbols, - system: { - checkOrderExecutePeriod, - temporaryDisableActionAfterConfirmingOrder - } + system: { temporaryDisableActionAfterConfirmingOrder } } } = data; @@ -304,7 +239,6 @@ const execute = async (logger, rawData) => { symbol, 'buy', lastBuyOrder, - lastBuyOrder, notifyOrderExecute ); @@ -320,137 +254,29 @@ const execute = async (logger, rawData) => { }, temporaryDisableActionAfterConfirmingOrder ); + } else if (removeStatuses.includes(lastBuyOrder.status)) { + logger.info( + { + lastBuyOrder, + saveLog: true + }, + `The order status "${lastBuyOrder.status}" is no longer valid. Delete the order to stop monitoring.` + ); + // If order is no longer available, then delete from cache + await removeGridTradeLastOrder(logger, symbols, symbol, 'buy'); + + slackMessageOrderDeleted( + logger, + symbol, + 'buy', + lastBuyOrder, + notifyOrderExecute + ); } else { - // If not filled, check orders is time to check or not - const nextCheck = _.get(lastBuyOrder, 'nextCheck', null); - if (moment(nextCheck) < moment()) { - // Check orders whether it's filled or not - let orderResult; - try { - orderResult = await binance.client.getOrder({ - symbol, - orderId: lastBuyOrder.orderId - }); - } catch (e) { - logger.error( - { e }, - 'The order could not be found or error occurred querying the order.' - ); - const updatedNextCheck = moment().add( - checkOrderExecutePeriod, - 'seconds' - ); - - logger.info( - { - e, - lastBuyOrder, - checkOrderExecutePeriod, - nextCheck: updatedNextCheck.format(), - saveLog: true - }, - 'The gird trade order could not be found or error occurred querying the order.' - ); - - // Set last order to be checked later - await updateGridTradeLastOrder(logger, symbol, 'buy', { - ...lastBuyOrder, - nextCheck: updatedNextCheck.format() - }); - - return data; - } - - // If filled, then calculate average cost and quantity and save new last buy pirce. - if (orderResult.status === 'FILLED') { - logger.info( - { lastBuyOrder, saveLog: true }, - 'The grid trade order is filled. Caluclating last buy price...' - ); - - // Calculate last buy price - await calculateLastBuyPrice(logger, symbol, orderResult); - - // Save grid trade to the database - await saveGridTrade(logger, data, { - ...lastBuyOrder, - ...orderResult - }); - - // Remove grid trade last order - await removeGridTradeLastOrder(logger, symbols, symbol, 'buy'); - - slackMessageOrderFilled( - logger, - symbol, - 'buy', - lastBuyOrder, - orderResult, - notifyOrderExecute - ); - - // Lock symbol action configured seconds to avoid immediate action - await disableAction( - logger, - symbol, - { - disabledBy: 'buy filled order', - message: - 'Disabled action after confirming filled grid trade order.', - canResume: false, - canRemoveLastBuyPrice: false - }, - temporaryDisableActionAfterConfirmingOrder - ); - } else if (removeStatuses.includes(orderResult.status) === true) { - logger.info( - { - orderResult, - saveLog: true - }, - `The order status "${orderResult.status}" is no longer valid. Delete the order to stop monitoring.` - ); - - // If order is no longer available, then delete from cache - await removeGridTradeLastOrder(logger, symbols, symbol, 'buy'); - - slackMessageOrderDeleted( - logger, - symbol, - 'buy', - lastBuyOrder, - orderResult, - notifyOrderExecute - ); - } else { - // If not filled, update next check time - const updatedNextCheck = moment().add( - checkOrderExecutePeriod, - 'seconds' - ); - - logger.info( - { - orderResult, - checkOrderExecutePeriod, - nextCheck: updatedNextCheck.format(), - saveLog: true - }, - 'The grid trade order is not filled.' - ); - - await updateGridTradeLastOrder(logger, symbol, 'buy', { - ...orderResult, - currentGridTradeIndex: lastBuyOrder.currentGridTradeIndex, - nextCheck: updatedNextCheck.format() - }); - } - } else { - logger.info( - { lastBuyOrder, nextCheck, currentTime: moment() }, - 'Skip checking the grid trade last buy order' - ); - } + logger.info( + { lastBuyOrder, currentTime: moment() }, + 'Skip checking the grid trade last buy order' + ); } } @@ -474,7 +300,6 @@ const execute = async (logger, rawData) => { symbol, 'sell', lastSellOrder, - lastSellOrder, notifyOrderExecute ); @@ -490,124 +315,22 @@ const execute = async (logger, rawData) => { }, temporaryDisableActionAfterConfirmingOrder ); + } else if (removeStatuses.includes(lastSellOrder.status)) { + // If order is no longer available, then delete from cache + await removeGridTradeLastOrder(logger, symbols, symbol, 'sell'); + + slackMessageOrderDeleted( + logger, + symbol, + 'sell', + lastSellOrder, + notifyOrderExecute + ); } else { - // If not filled, check orders is time to check or not - const nextCheck = _.get(lastSellOrder, 'nextCheck', null); - - if (moment(nextCheck) < moment()) { - // Check orders whether it's filled or not - let orderResult; - try { - orderResult = await binance.client.getOrder({ - symbol, - orderId: lastSellOrder.orderId - }); - } catch (e) { - logger.error( - { e }, - 'The order could not be found or error occurred querying the order.' - ); - const updatedNextCheck = moment().add( - checkOrderExecutePeriod, - 'seconds' - ); - - logger.info( - { - e, - lastSellOrder, - checkOrderExecutePeriod, - nextCheck: updatedNextCheck.format(), - saveLog: true - }, - 'The grid trade order could not be found or error occurred querying the order.' - ); - - // Set last order to be checked later - await updateGridTradeLastOrder(logger, symbol, 'sell', { - ...lastSellOrder, - nextCheck: updatedNextCheck.format() - }); - - return data; - } - - // If filled, then calculate average cost and quantity and save new last buy pirce. - if (orderResult.status === 'FILLED') { - logger.info({ lastSellOrder }, 'The order is filled.'); - - // Save grid trade to the database - await saveGridTrade(logger, data, { - ...lastSellOrder, - ...orderResult - }); - - // Remove grid trade last order - await removeGridTradeLastOrder(logger, symbols, symbol, 'sell'); - - slackMessageOrderFilled( - logger, - symbol, - 'sell', - lastSellOrder, - orderResult, - notifyOrderExecute - ); - - // Lock symbol action configured seconds to avoid immediate action - await disableAction( - logger, - symbol, - { - disabledBy: 'sell filled order', - message: - 'Disabled action after confirming filled grid trade order.', - canResume: false, - canRemoveLastBuyPrice: true - }, - temporaryDisableActionAfterConfirmingOrder - ); - } else if (removeStatuses.includes(orderResult.status) === true) { - // If order is no longer available, then delete from cache - await removeGridTradeLastOrder(logger, symbols, symbol, 'sell'); - - slackMessageOrderDeleted( - logger, - symbol, - 'sell', - lastSellOrder, - orderResult, - notifyOrderExecute - ); - } else { - // If not filled, update next check time - const updatedNextCheck = moment().add( - checkOrderExecutePeriod, - 'seconds' - ); - - logger.info( - { - orderResult, - checkOrderExecutePeriod, - nextCheck: updatedNextCheck.format(), - saveLog: true - }, - 'The grid trade order is not filled.' - ); - - await updateGridTradeLastOrder(logger, symbol, 'sell', { - ...orderResult, - currentGridTradeIndex: lastSellOrder.currentGridTradeIndex, - nextCheck: updatedNextCheck.format() - }); - } - } else { - logger.info( - { lastSellOrder, nextCheck, currentTime: moment() }, - 'Skip checking the grid trade last buy order' - ); - } + logger.info( + { lastSellOrder, currentTime: moment() }, + 'Skip checking the grid trade last sell order' + ); } } diff --git a/app/cronjob/trailingTrade/step/ensure-manual-order.js b/app/cronjob/trailingTrade/step/ensure-manual-order.js index e666c11f..34456d25 100644 --- a/app/cronjob/trailingTrade/step/ensure-manual-order.js +++ b/app/cronjob/trailingTrade/step/ensure-manual-order.js @@ -1,7 +1,7 @@ /* eslint-disable no-await-in-loop */ const moment = require('moment'); const _ = require('lodash'); -const { PubSub, binance, slack } = require('../../../helpers'); +const { PubSub, slack } = require('../../../helpers'); const { calculateLastBuyPrice, getAPILimit, @@ -15,8 +15,7 @@ const { const { getManualOrders, - deleteManualOrder, - saveManualOrder + deleteManualOrder } = require('../../trailingTradeHelper/order'); /** @@ -25,17 +24,10 @@ const { * @param {*} logger * @param {*} symbol * @param {*} side - * @param {*} orderParams - * @param {*} orderResult + * @param {*} order */ -const slackMessageOrderFilled = async ( - logger, - symbol, - side, - orderParams, - orderResult -) => { - const type = orderParams.type.toUpperCase(); +const slackMessageOrderFilled = async (logger, symbol, side, order) => { + const type = order.type.toUpperCase(); PubSub.publish('frontend-notification', { type: 'success', @@ -46,11 +38,7 @@ const slackMessageOrderFilled = async ( `${symbol} Manual ${side.toUpperCase()} Order Filled (${moment().format( 'HH:mm:ss.SSS' )}): *${type}*\n` + - `- Order Result: \`\`\`${JSON.stringify( - orderResult, - undefined, - 2 - )}\`\`\`\n` + + `- Order Result: \`\`\`${JSON.stringify(order, undefined, 2)}\`\`\`\n` + `- Current API Usage: ${getAPILimit(logger)}` ); }; @@ -100,32 +88,21 @@ const saveGridTrade = async (logger, rawData, order) => { * @param {*} logger * @param {*} symbol * @param {*} side - * @param {*} orderParams - * @param {*} orderResult + * @param {*} order */ -const slackMessageOrderDeleted = async ( - logger, - symbol, - side, - orderParams, - orderResult -) => { - const type = orderParams.type.toUpperCase(); +const slackMessageOrderDeleted = async (logger, symbol, side, order) => { + const type = order.type.toUpperCase(); PubSub.publish('frontend-notification', { type: 'success', - title: `The ${side} order for ${symbol} is ${orderResult.status}. Stop monitoring.` + title: `The ${side} order for ${symbol} is ${order.status}. Stop monitoring.` }); return slack.sendMessage( `${symbol} Manual ${side.toUpperCase()} Order Removed (${moment().format( 'HH:mm:ss.SSS' )}): *${type}*\n` + - `- Order Result: \`\`\`${JSON.stringify( - orderResult, - undefined, - 2 - )}\`\`\`\n` + + `- Order Result: \`\`\`${JSON.stringify(order, undefined, 2)}\`\`\`\n` + `- Current API Usage: ${getAPILimit(logger)}` ); }; @@ -139,12 +116,7 @@ const slackMessageOrderDeleted = async ( const execute = async (logger, rawData) => { const data = rawData; - const { - symbol, - symbolConfiguration: { - system: { checkManualOrderPeriod } - } - } = data; + const { symbol } = data; if (isExceedAPILimit(logger)) { logger.info('The API limit is exceed, do not try to ensure manual order.'); @@ -184,119 +156,15 @@ const execute = async (logger, rawData) => { await saveGridTrade(logger, data, order); await deleteManualOrder(logger, symbol, order.orderId); - } else { - // If not filled, check orders is time to check or not - const nextCheck = _.get(order, 'nextCheck', null); - - if (moment(nextCheck) < moment()) { - // Check orders whether it's filled or not - let orderResult; - try { - orderResult = await binance.client.getOrder({ - symbol, - orderId: order.orderId - }); - } catch (e) { - logger.error( - { e }, - 'The order could not be found or error occurred querying the order.' - ); - const updatedNextCheck = moment().add( - checkManualOrderPeriod, - 'seconds' - ); - - logger.info( - { - e, - order, - checkManualOrderPeriod, - nextCheck: updatedNextCheck.format(), - saveLog: true - }, - 'The manual order could not be found or error occurred querying the order.' - ); - - await saveManualOrder(logger, symbol, order.orderId, { - ...order, - nextCheck: updatedNextCheck.format() - }); - - return data; - } - // If filled, then calculate average cost and quantity and save new last buy pirce. - if (orderResult.status === 'FILLED') { - logger.info( - { order, saveLog: true }, - 'The manual order is filled. Caluclating last buy price...' - ); - - // Calulate last buy price - if (orderResult.side === 'BUY') { - await calculateLastBuyPrice(logger, symbol, orderResult); - } - - // Save grid trade to the database - await saveGridTrade(logger, data, orderResult); - - // Remove manual buy order - await deleteManualOrder(logger, symbol, orderResult.orderId); - - slackMessageOrderFilled( - logger, - symbol, - order.side, - order, - orderResult - ); - } else if (removeStatuses.includes(orderResult.status) === true) { - // If order is no longer available, then delete from cache - logger.info( - { - orderResult, - saveLog: true - }, - 'The manual order status is no longer valid. Delete the manual order.' - ); - - await deleteManualOrder(logger, symbol, orderResult.orderId); - - slackMessageOrderDeleted( - logger, - symbol, - order.side, - order, - orderResult - ); - } else { - // If not filled, update next check time - const updatedNextCheck = moment().add( - checkManualOrderPeriod, - 'seconds' - ); - - logger.info( - { - orderResult, - checkManualOrderPeriod, - nextCheck: updatedNextCheck.format(), - saveLog: true - }, - 'The manual order is not filled.' - ); + slackMessageOrderFilled(logger, symbol, order.side, order); + } else if (removeStatuses.includes(order.status)) { + // If order is no longer available, then delete from cache + await deleteManualOrder(logger, symbol, order.orderId); - await saveManualOrder(logger, symbol, orderResult.orderId, { - ...orderResult, - nextCheck: updatedNextCheck.format() - }); - } - } else { - logger.info( - { order, nextCheck, currentTime: moment() }, - 'Skip checking the order' - ); - } + slackMessageOrderDeleted(logger, symbol, order.side, order); + } else { + logger.info({ order, currentTime: moment() }, 'Skip checking the order'); } } diff --git a/app/cronjob/trailingTrade/step/get-balances.js b/app/cronjob/trailingTrade/step/get-balances.js index 56e085e7..f9e83cbe 100644 --- a/app/cronjob/trailingTrade/step/get-balances.js +++ b/app/cronjob/trailingTrade/step/get-balances.js @@ -46,7 +46,7 @@ const execute = async (logger, rawData) => { data.baseAssetBalance = baseAssetBalance; data.baseAssetBalance.total = baseAssetTotalBalance; data.baseAssetBalance.estimatedValue = baseAssetEstimatedValue; - data.baseAssetBalance.updatedAt = moment(accountInfo.updateTime).format(); + data.baseAssetBalance.updatedAt = moment(accountInfo.updateTime).toDate(); data.quoteAssetBalance = quoteAssetBalance; diff --git a/app/cronjob/trailingTrade/step/get-indicators.js b/app/cronjob/trailingTrade/step/get-indicators.js index 3211040b..859574fe 100644 --- a/app/cronjob/trailingTrade/step/get-indicators.js +++ b/app/cronjob/trailingTrade/step/get-indicators.js @@ -1,9 +1,35 @@ /* eslint-disable prefer-destructuring */ const _ = require('lodash'); const moment = require('moment'); -const { cache } = require('../../../helpers'); +const { cache, mongo } = require('../../../helpers'); const { getLastBuyPrice } = require('../../trailingTradeHelper/common'); +/** + * Flatten candle data + * + * @param {*} candles + */ +const flattenCandlesData = candles => { + const openTime = []; + const high = []; + const low = []; + const close = []; + + candles.forEach(candle => { + openTime.push(+candle.openTime); + high.push(+candle.high); + low.push(+candle.low); + close.push(+candle.close); + }); + + return { + openTime, + high, + low, + close + }; +}; + /** * Get symbol information, buy/sell indicators * @@ -19,11 +45,16 @@ const execute = async (logger, rawData) => { filterMinNotional: { minNotional } }, symbolConfiguration: { + candles: { limit: candlesLimit }, buy: { currentGridTradeIndex: currentBuyGridTradeIndex, currentGridTrade: currentBuyGridTrade, athRestriction: { enabled: buyATHRestrictionEnabled, + candles: { + limit: buyATHRestrictionCandlesLimit, + interval: buyATHRestrictionCandlesInterval + }, restrictionPercentage: buyATHRestrictionPercentage } }, @@ -36,24 +67,104 @@ const execute = async (logger, rawData) => { openOrders } = data; - const cachedIndicator = - JSON.parse( - await cache.hget('trailing-trade-symbols', `${symbol}-indicator-data`) - ) || {}; + const candles = _.orderBy( + await mongo.findAll( + logger, + 'trailing-trade-candles', + { + key: `${symbol}` + }, + { + sort: { + time: -1 + }, + limit: candlesLimit + } + ), + ['time'], + ['desc'] + ); - if (_.isEmpty(cachedIndicator)) { - logger.info('Indicator data is not retrieved, wait for cache.'); + if (_.isEmpty(candles)) { data.saveToCache = false; return data; } + // Flatten candles data to get lowest price + const candlesData = flattenCandlesData(candles); + + // Get the lowest price + const lowestPrice = _.min(candlesData.low); + + const highestPrice = _.max(candlesData.high); + + // Retrieve ATH candles + let athPrice = null; + + if (buyATHRestrictionEnabled) { + logger.info( + { + debug: true, + function: 'athCandles', + buyATHRestrictionEnabled, + buyATHRestrictionCandlesInterval, + buyATHRestrictionCandlesLimit + }, + 'Retrieving ATH candles from MongoDB' + ); + + const athCandles = _.orderBy( + await mongo.findAll( + logger, + 'trailing-trade-ath-candles', + { + key: `${symbol}` + }, + { + sort: { + time: -1 + }, + limit: buyATHRestrictionCandlesLimit + } + ), + ['time'], + ['desc'] + ); + + // Flatten candles data to get ATH price + const athCandlesData = flattenCandlesData(athCandles); + + // ATH (All The High) price + athPrice = _.max(athCandlesData.high); + } else { + logger.info( + { + debug: true, + function: 'athCandles', + buyATHRestrictionEnabled, + buyATHRestrictionCandlesInterval, + buyATHRestrictionCandlesLimit + }, + 'ATH Restriction is disabled' + ); + } + + const latestIndicators = { + highestPrice, + lowestPrice, + athPrice + }; + const cachedLatestCandle = JSON.parse( await cache.hget('trailing-trade-symbols', `${symbol}-latest-candle`) ) || {}; if (_.isEmpty(cachedLatestCandle)) { - logger.info('Last candle is not retrieved, wait for cache.'); + logger.info( + { saveLog: true }, + 'Last candle is not retrieved. The action cannot be proceed. Any override action will be removed.' + ); data.saveToCache = false; return data; } @@ -70,11 +181,9 @@ const execute = async (logger, rawData) => { // Merge indicator data data.indicators = { ...data.indicators, - ...cachedIndicator + ...latestIndicators }; - const { highestPrice, lowestPrice, athPrice } = data.indicators; - // Get current price const currentPrice = parseFloat(cachedLatestCandle.close); @@ -154,7 +263,7 @@ const execute = async (logger, rawData) => { const newOpenOrders = openOrders.map(order => { const newOrder = order; newOrder.currentPrice = currentPrice; - newOrder.updatedAt = moment(order.time).utc(); + newOrder.updatedAt = moment(order.time).utc().toDate(); if (order.type !== 'STOP_LOSS_LIMIT') { return newOrder; @@ -205,7 +314,7 @@ const execute = async (logger, rawData) => { difference: buyDifference, openOrders: newOpenOrders?.filter(o => o.side.toLowerCase() === 'buy'), processMessage: _.get(data, 'buy.processMessage', ''), - updatedAt: moment().utc() + updatedAt: moment().utc().toDate() }; data.sell = { @@ -220,7 +329,7 @@ const execute = async (logger, rawData) => { currentProfitPercentage: sellCurrentProfitPercentage, openOrders: newOpenOrders?.filter(o => o.side.toLowerCase() === 'sell'), processMessage: _.get(data, 'sell.processMessage', ''), - updatedAt: moment().utc() + updatedAt: moment().utc().toDate() }; return data; diff --git a/app/cronjob/trailingTrade/step/handle-open-orders.js b/app/cronjob/trailingTrade/step/handle-open-orders.js index 5a8aadf5..f524b1d4 100644 --- a/app/cronjob/trailingTrade/step/handle-open-orders.js +++ b/app/cronjob/trailingTrade/step/handle-open-orders.js @@ -1,11 +1,13 @@ /* eslint-disable no-await-in-loop */ const moment = require('moment'); +const _ = require('lodash'); const { binance } = require('../../../helpers'); const { - getAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI, - saveOverrideAction + getAccountInfo, + updateAccountInfo, + saveOverrideAction, + getAndCacheOpenOrdersForSymbol } = require('../../trailingTradeHelper/common'); /** @@ -57,7 +59,8 @@ const execute = async (logger, rawData) => { isLocked, openOrders, buy: { limitPrice: buyLimitPrice }, - sell: { limitPrice: sellLimitPrice } + sell: { limitPrice: sellLimitPrice }, + symbolInfo: { quoteAsset, baseAsset } } = data; if (isLocked) { @@ -107,7 +110,7 @@ const execute = async (logger, rawData) => { ); // Refresh account info - data.accountInfo = await getAccountInfoFromAPI(logger); + data.accountInfo = await getAccountInfo(logger); data.action = 'buy-order-checking'; @@ -130,8 +133,28 @@ const execute = async (logger, rawData) => { // Set action as buy data.action = 'buy'; - // Get account information again because the order is cancelled - data.accountInfo = await getAccountInfoFromAPI(logger); + const orderAmount = order.origQty * order.price; + + // Immediately update the balance of "quote" asset when the order is canceled so that + // we don't have to wait for the websocket because the next action is buy + const balances = [ + { + asset: quoteAsset, + free: + _.toNumber(data.quoteAssetBalance.free) + + _.toNumber(orderAmount), + locked: + _.toNumber(data.quoteAssetBalance.locked) - + _.toNumber(orderAmount) + } + ]; + + // Refresh account info + data.accountInfo = await updateAccountInfo( + logger, + balances, + moment().utc().format() + ); } } else { logger.info( @@ -168,7 +191,7 @@ const execute = async (logger, rawData) => { ); // Refresh account info - data.accountInfo = await getAccountInfoFromAPI(logger); + data.accountInfo = await getAccountInfo(logger); data.action = 'sell-order-checking'; } else { @@ -178,8 +201,26 @@ const execute = async (logger, rawData) => { // Set action as sell data.action = 'sell'; - // Get account information again because the order is cancelled - data.accountInfo = await getAccountInfoFromAPI(logger); + // Immediately update the balance of "base" asset when the order is canceled so that + // we don't have to wait for the websocket because the next action is sell + const balances = [ + { + asset: baseAsset, + free: + _.toNumber(data.baseAssetBalance.free) + + _.toNumber(order.origQty), + locked: + _.toNumber(data.baseAssetBalance.locked) - + _.toNumber(order.origQty) + } + ]; + + // Refresh account info + data.accountInfo = await updateAccountInfo( + logger, + balances, + moment().utc().format() + ); } } else { logger.info( diff --git a/app/cronjob/trailingTrade/step/place-buy-order.js b/app/cronjob/trailingTrade/step/place-buy-order.js index 6fee8602..c44608c5 100644 --- a/app/cronjob/trailingTrade/step/place-buy-order.js +++ b/app/cronjob/trailingTrade/step/place-buy-order.js @@ -2,12 +2,12 @@ const _ = require('lodash'); const moment = require('moment'); const { binance, slack } = require('../../../helpers'); const { - getAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI, + updateAccountInfo, isExceedAPILimit, getAPILimit, saveOrderStats, - saveOverrideAction + saveOverrideAction, + getAndCacheOpenOrdersForSymbol } = require('../../trailingTradeHelper/common'); const { saveGridTradeOrder } = require('../../trailingTradeHelper/order'); @@ -143,7 +143,7 @@ const setMessage = (logger, rawData, processMessage) => { logger.info({ data, saveLog: true }, processMessage); data.buy.processMessage = processMessage; - data.buy.updatedAt = moment().utc(); + data.buy.updatedAt = moment().utc().toDate(); return data; }; @@ -169,8 +169,7 @@ const execute = async (logger, rawData) => { }, symbolConfiguration: { symbols, - buy: { enabled: tradingEnabled, currentGridTradeIndex, currentGridTrade }, - system: { checkOrderExecutePeriod } + buy: { enabled: tradingEnabled, currentGridTradeIndex, currentGridTrade } }, action, quoteAssetBalance: { free: quoteAssetFreeBalance }, @@ -312,11 +311,9 @@ const execute = async (logger, rawData) => { lotStepSizePrecision ) ); - // If free balance is exactly same as minimum notional, then it will be failed to place the order // because it will be always less than minimum notional after calculating commission. // To avoid the minimum notional issue, add commission to free balance - if ( orgFreeBalance > parseFloat(minNotional) && maxPurchaseAmount === parseFloat(minNotional) @@ -335,21 +332,19 @@ const execute = async (logger, rawData) => { ) ); } - logger.info({ orderQuantity }, 'Order quantity after commission'); - if (orderQuantity * limitPrice < parseFloat(minNotional)) { + const orderAmount = orderQuantity * limitPrice; + + if (orderAmount < parseFloat(minNotional)) { const processMessage = `Do not place a buy order for the grid trade #${humanisedGridTradeIndex} ` + `as not enough ${quoteAsset} ` + `to buy ${baseAsset} after calculating commission - Order amount: ${_.floor( - orderQuantity * limitPrice, + orderAmount, priceTickPrecision )} ${quoteAsset}, Minimum notional: ${minNotional}.`; - logger.info( - { calculatedAmount: orderQuantity * limitPrice, minNotional }, - processMessage - ); + logger.info({ calculatedAmount: orderAmount, minNotional }, processMessage); return setMessage(logger, data, processMessage); } @@ -438,8 +433,7 @@ const execute = async (logger, rawData) => { // Set last buy grid order to be checked until it is executed await saveGridTradeOrder(logger, `${symbol}-grid-trade-last-buy-order`, { ...orderResult, - currentGridTradeIndex, - nextCheck: moment().add(checkOrderExecutePeriod, 'seconds').format() + currentGridTradeIndex }); // Save number of open orders @@ -451,8 +445,25 @@ const execute = async (logger, rawData) => { o => o.side.toLowerCase() === 'buy' ); + // Immediately update the balance of quote asset when the order is canceled so that + // we don't have to wait for the websocket and to avoid the race condition + const balances = [ + { + asset: quoteAsset, + free: _.toNumber(data.quoteAssetBalance.free) - _.toNumber(orderAmount), + locked: + _.toNumber(data.quoteAssetBalance.locked) + _.toNumber(orderAmount) + } + ]; + + logger.info({ balances }, 'Received new user activity'); + // Refresh account info - data.accountInfo = await getAccountInfoFromAPI(logger); + data.accountInfo = await updateAccountInfo( + logger, + balances, + moment().utc().format() + ); slack.sendMessage( `${symbol} Buy Action Grid Trade #${humanisedGridTradeIndex} Result (${moment().format( diff --git a/app/cronjob/trailingTrade/step/place-manual-trade.js b/app/cronjob/trailingTrade/step/place-manual-trade.js index 227826a1..2005315c 100644 --- a/app/cronjob/trailingTrade/step/place-manual-trade.js +++ b/app/cronjob/trailingTrade/step/place-manual-trade.js @@ -2,8 +2,8 @@ const moment = require('moment'); const { binance, slack, PubSub } = require('../../../helpers'); const { getAPILimit, - getAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI + getAccountInfo, + getAndCacheOpenOrdersForSymbol } = require('../../trailingTradeHelper/common'); const { saveManualOrder } = require('../../trailingTradeHelper/order'); @@ -20,7 +20,7 @@ const setMessage = (logger, rawData, processMessage) => { logger.info({ data, saveLog: true }, processMessage); data.buy.processMessage = processMessage; - data.buy.updatedAt = moment().utc(); + data.buy.updatedAt = moment().utc().toDate(); return data; }; @@ -220,17 +220,15 @@ const slackMessageOrderResult = async ( * * @param {*} logger * @param {*} orderResult - * @param {*} checkManualOrderPeriod */ -const recordOrder = async (logger, orderResult, checkManualOrderPeriod) => { +const recordOrder = async (logger, orderResult) => { const { symbol, orderId } = orderResult; // Save manual order logger.info({ orderResult }, 'Record order'); await saveManualOrder(logger, symbol, orderId, { - ...orderResult, - nextCheck: moment().add(checkManualOrderPeriod, 'seconds').format() + ...orderResult }); }; @@ -242,16 +240,7 @@ const recordOrder = async (logger, orderResult, checkManualOrderPeriod) => { */ const execute = async (logger, rawData) => { const data = rawData; - const { - symbol, - isLocked, - action, - baseAssetBalance, - symbolConfiguration: { - system: { checkManualOrderPeriod } - }, - order - } = data; + const { symbol, isLocked, action, baseAssetBalance, order } = data; if (isLocked) { logger.info({ isLocked }, 'Symbol is locked, do not process manual-trade'); @@ -276,7 +265,7 @@ const execute = async (logger, rawData) => { logger.info({ orderResult }, 'Manual order result'); - await recordOrder(logger, orderResult, checkManualOrderPeriod); + await recordOrder(logger, orderResult); // Get open orders and update cache data.openOrders = await getAndCacheOpenOrdersForSymbol(logger, symbol); @@ -287,7 +276,7 @@ const execute = async (logger, rawData) => { o => o.side.toLowerCase() === 'sell' ); // Refresh account info - data.accountInfo = await getAccountInfoFromAPI(logger); + data.accountInfo = await getAccountInfo(logger); slackMessageOrderResult(logger, symbol, order.side, order, orderResult); diff --git a/app/cronjob/trailingTrade/step/place-sell-order.js b/app/cronjob/trailingTrade/step/place-sell-order.js index b424fdf4..e7b91844 100644 --- a/app/cronjob/trailingTrade/step/place-sell-order.js +++ b/app/cronjob/trailingTrade/step/place-sell-order.js @@ -3,10 +3,10 @@ const moment = require('moment'); const { binance, slack } = require('../../../helpers'); const { roundDown } = require('../../trailingTradeHelper/util'); const { - getAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI, + getAccountInfo, isExceedAPILimit, - getAPILimit + getAPILimit, + getAndCacheOpenOrdersForSymbol } = require('../../trailingTradeHelper/common'); const { saveGridTradeOrder } = require('../../trailingTradeHelper/order'); @@ -23,7 +23,7 @@ const setMessage = (logger, rawData, processMessage) => { logger.info({ data, saveLog: true }, processMessage); data.sell.processMessage = processMessage; - data.sell.updatedAt = moment().utc(); + data.sell.updatedAt = moment().utc().toDate(); return data; }; @@ -45,12 +45,7 @@ const execute = async (logger, rawData) => { filterMinNotional: { minNotional } }, symbolConfiguration: { - sell: { - enabled: tradingEnabled, - currentGridTradeIndex, - currentGridTrade - }, - system: { checkOrderExecutePeriod } + sell: { enabled: tradingEnabled, currentGridTradeIndex, currentGridTrade } }, action, baseAssetBalance: { free: baseAssetFreeBalance }, @@ -103,7 +98,6 @@ const execute = async (logger, rawData) => { logger.info({ freeBalance }, 'Free balance'); // If after calculating quantity percentage, it is not enough minimum notional, then simply sell all balance - let orderQuantity = parseFloat( _.floor(freeBalance - freeBalance * (0.1 / 100), lotPrecision) ); @@ -116,6 +110,7 @@ const execute = async (logger, rawData) => { lotPrecision ) ); + logger.info( { orderQuantityWithPercentage: orderQuantity }, 'Calculated order quantity with quantity percentage.' @@ -203,8 +198,7 @@ const execute = async (logger, rawData) => { await saveGridTradeOrder(logger, `${symbol}-grid-trade-last-sell-order`, { ...orderResult, - currentGridTradeIndex, - nextCheck: moment().add(checkOrderExecutePeriod, 'seconds').format() + currentGridTradeIndex }); // Get open orders and update cache @@ -214,7 +208,7 @@ const execute = async (logger, rawData) => { ); // Refresh account info - data.accountInfo = await getAccountInfoFromAPI(logger); + data.accountInfo = await getAccountInfo(logger); slack.sendMessage( `${symbol} Sell Action Grid Trade #${humanisedGridTradeIndex} Result (${moment().format( diff --git a/app/cronjob/trailingTrade/step/place-sell-stop-loss-order.js b/app/cronjob/trailingTrade/step/place-sell-stop-loss-order.js index 37cbe70d..70bce7cf 100644 --- a/app/cronjob/trailingTrade/step/place-sell-stop-loss-order.js +++ b/app/cronjob/trailingTrade/step/place-sell-stop-loss-order.js @@ -2,11 +2,11 @@ const _ = require('lodash'); const moment = require('moment'); const { binance, slack } = require('../../../helpers'); const { - getAndCacheOpenOrdersForSymbol, - getAccountInfoFromAPI, + getAccountInfo, isExceedAPILimit, disableAction, - getAPILimit + getAPILimit, + getAndCacheOpenOrdersForSymbol } = require('../../trailingTradeHelper/common'); const { saveSymbolGridTrade @@ -25,7 +25,7 @@ const setMessage = (logger, rawData, processMessage) => { logger.info({ data, saveLog: true }, processMessage); data.sell.processMessage = processMessage; - data.sell.updatedAt = moment().utc(); + data.sell.updatedAt = moment().utc().toDate(); return data; }; @@ -202,7 +202,7 @@ const execute = async (logger, rawData) => { ); // Refresh account info - data.accountInfo = await getAccountInfoFromAPI(logger); + data.accountInfo = await getAccountInfo(logger); slack.sendMessage( `${symbol} Sell Stop-Loss Action Result (${moment().format( diff --git a/app/cronjob/trailingTrade/step/remove-last-buy-price.js b/app/cronjob/trailingTrade/step/remove-last-buy-price.js index 3f471b70..6e5f9fc1 100644 --- a/app/cronjob/trailingTrade/step/remove-last-buy-price.js +++ b/app/cronjob/trailingTrade/step/remove-last-buy-price.js @@ -2,12 +2,12 @@ const _ = require('lodash'); const moment = require('moment'); const { slack, PubSub } = require('../../../helpers'); const { - getAndCacheOpenOrdersForSymbol, getAPILimit, isActionDisabled, removeLastBuyPrice: removeLastBuyPriceFromDatabase, saveOrderStats, - saveOverrideAction + saveOverrideAction, + getAndCacheOpenOrdersForSymbol } = require('../../trailingTradeHelper/common'); const { archiveSymbolGridTrade, @@ -28,7 +28,7 @@ const setMessage = (logger, rawData, processMessage) => { logger.info({ data, saveLog: true }, processMessage); data.sell.processMessage = processMessage; - data.sell.updatedAt = moment().utc(); + data.sell.updatedAt = moment().utc().toDate(); return data; }; diff --git a/app/cronjob/trailingTrade/step/save-data-to-cache.js b/app/cronjob/trailingTrade/step/save-data-to-cache.js index 544bab86..c7428399 100644 --- a/app/cronjob/trailingTrade/step/save-data-to-cache.js +++ b/app/cronjob/trailingTrade/step/save-data-to-cache.js @@ -1,4 +1,5 @@ -const { cache } = require('../../../helpers'); +const _ = require('lodash'); +const { mongo } = require('../../../helpers'); /** * Save data to cache @@ -19,11 +20,17 @@ const execute = async (logger, rawData) => { return data; } - cache.hset( - 'trailing-trade-symbols', - `${symbol}-processed-data`, - JSON.stringify(data) - ); + const filter = { symbol }; + + const document = _.omit(data, [ + 'closedTrades', + 'accountInfo', + 'symbolConfiguration.symbols', + 'tradingView' + ]); + + await mongo.upsertOne(logger, 'trailing-trade-cache', filter, document); + return data; }; diff --git a/app/cronjob/trailingTradeHelper/__tests__/common.test.js b/app/cronjob/trailingTradeHelper/__tests__/common.test.js index 576c9ee8..9440e64b 100644 --- a/app/cronjob/trailingTradeHelper/__tests__/common.test.js +++ b/app/cronjob/trailingTradeHelper/__tests__/common.test.js @@ -453,6 +453,63 @@ describe('common.js', () => { }); }); + describe('getCachedExchangeSymbols', () => { + describe('when exchange symbols is not null', () => { + beforeEach(async () => { + const { cache, logger } = require('../../../helpers'); + + cacheMock = cache; + + cacheMock.hget = jest + .fn() + .mockResolvedValue( + JSON.stringify( + require('./fixtures/binance-cached-exchange-symbols.json') + ) + ); + + commonHelper = require('../common'); + result = await commonHelper.getCachedExchangeSymbols(logger); + }); + + it('triggers cache.hget', () => { + expect(cacheMock.hget).toHaveBeenCalledWith( + 'trailing-trade-common', + 'exchange-symbols' + ); + }); + + it('returns expected value', () => { + expect(result).toStrictEqual( + require('./fixtures/binance-cached-exchange-symbols.json') + ); + }); + }); + describe('when exchange symbols is null', () => { + beforeEach(async () => { + const { cache, logger } = require('../../../helpers'); + + cacheMock = cache; + + cacheMock.hget = jest.fn().mockResolvedValue(null); + + commonHelper = require('../common'); + result = await commonHelper.getCachedExchangeSymbols(logger); + }); + + it('triggers cache.hget', () => { + expect(cacheMock.hget).toHaveBeenCalledWith( + 'trailing-trade-common', + 'exchange-symbols' + ); + }); + + it('returns expected value', () => { + expect(result).toStrictEqual({}); + }); + }); + }); + describe('getAccountInfo', () => { describe('when there is cached account information', () => { beforeEach(async () => { @@ -2448,4 +2505,463 @@ describe('common.js', () => { }); }); }); + + describe('saveCandle', () => { + beforeEach(async () => { + const { mongo, logger } = require('../../../helpers'); + + mongoMock = mongo; + loggerMock = logger; + + mongoMock.upsertOne = jest.fn().mockResolvedValue(true); + + commonHelper = require('../common'); + /** + * Sample candle + { + eventType: 'kline', + eventTime: 1657974109931, + symbol: 'BTCUSDT', + startTime: 1657972800000, + closeTime: 1657976399999, + firstTradeId: 1522267, + lastTradeId: 1524026, + open: '20616.72000000', + high: '20630.48000000', + low: '20595.11000000', + close: '20629.04000000', + volume: '93.17161900', + trades: 1760, + interval: '1h', + isFinal: false, + quoteVolume: '1920563.72755657', + buyVolume: '53.56450300', + quoteBuyVolume: '1104135.90026963' + } + */ + result = await commonHelper.saveCandle( + loggerMock, + 'trailing-trade-candles', + { + key: 'BTCUSDT', + interval: 10, + time: 1657972800000, + open: 20616.72, + high: 20630.48, + low: 20595.11, + close: 20629.04, + volume: 93.171619 + } + ); + }); + + it('triggers mongo.upsertOne', () => { + expect(mongoMock.upsertOne).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-candles', + { + key: 'BTCUSDT', + time: 1657972800000, + interval: 10 + }, + { + key: 'BTCUSDT', + interval: 10, + time: 1657972800000, + open: 20616.72, + high: 20630.48, + low: 20595.11, + close: 20629.04, + volume: 93.171619 + } + ); + }); + }); + + describe('updateAccountInfo', () => { + beforeEach(() => { + const { mongo, cache, logger } = require('../../../helpers'); + + cacheMock = cache; + mongoMock = mongo; + loggerMock = logger; + cacheMock.hset = jest.fn().mockResolvedValue(true); + }); + + describe('when there is no 0 balance', () => { + beforeEach(async () => { + cacheMock.hgetWithoutLock = jest + .fn() + .mockResolvedValue( + JSON.stringify( + require('./fixtures/binance-cached-account-info.json') + ) + ); + + commonHelper = require('../common'); + + result = await commonHelper.updateAccountInfo( + loggerMock, + [ + { + asset: 'USDT', + free: 9000, + locked: 1000 + } + ], + '2022-07-16T00:00:00' + ); + }); + + it('triggers cache.hset', () => { + expect(cacheMock.hset).toHaveBeenCalledWith( + 'trailing-trade-common', + 'account-info', + JSON.stringify({ + makerCommission: 0, + takerCommission: 0, + buyerCommission: 0, + sellerCommission: 0, + canTrade: true, + canWithdraw: false, + canDeposit: false, + updateTime: '2022-07-16T00:00:00', + accountType: 'SPOT', + balances: [ + { asset: 'BNB', free: '1000.00000000', locked: '0.00000000' }, + { asset: 'ETH', free: '100.00000000', locked: '0.00000000' }, + { asset: 'TRX', free: '500000.00000000', locked: '0.00000000' }, + { asset: 'USDT', free: 9000, locked: 1000 }, + { asset: 'XRP', free: '50000.00000000', locked: '0.00000000' } + ], + permissions: ['SPOT'] + }) + ); + }); + }); + + describe('when there is 0 balance', () => { + beforeEach(async () => { + cacheMock.hgetWithoutLock = jest.fn().mockResolvedValue( + JSON.stringify({ + updateTime: '2022-07-16T00:00:00', + balances: [ + { asset: 'BNB', free: '0', locked: '0' }, + { asset: 'ETH', free: '100.00000000', locked: '0.00000000' }, + { asset: 'TRX', free: '500000.00000000', locked: '0.00000000' }, + { asset: 'USDT', free: '9000.00000000', locked: '1000.00000000' }, + { asset: 'XRP', free: '50000.00000000', locked: '0.00000000' } + ] + }) + ); + + commonHelper = require('../common'); + + result = await commonHelper.updateAccountInfo( + loggerMock, + [ + { + asset: 'USDT', + free: 9000, + locked: 1000 + } + ], + '2022-07-16T00:00:00' + ); + }); + + it('triggers cache.hset', () => { + expect(cacheMock.hset).toHaveBeenCalledWith( + 'trailing-trade-common', + 'account-info', + JSON.stringify({ + updateTime: '2022-07-16T00:00:00', + balances: [ + { asset: 'ETH', free: '100.00000000', locked: '0.00000000' }, + { asset: 'TRX', free: '500000.00000000', locked: '0.00000000' }, + { asset: 'USDT', free: 9000, locked: 1000 }, + { asset: 'XRP', free: '50000.00000000', locked: '0.00000000' } + ] + }) + ); + }); + }); + }); + + describe('getCacheTrailingTradeSymbols', () => { + [ + { + desc: 'default', + sortByDesc: false, + sortByParam: null, + searchKeyword: 'BTC', + sortField: { + $cond: { + if: { $gt: [{ $size: '$buy.openOrders' }, 0] }, + then: { + $multiply: [ + { + $add: [ + { + $let: { + vars: { + buyOpenOrder: { + $arrayElemAt: ['$buy.openOrders', 0] + } + }, + in: '$buyOpenOrder.differenceToCancel' + } + }, + 3000 + ] + }, + -10 + ] + }, + else: { + $cond: { + if: { $gt: [{ $size: '$sell.openOrders' }, 0] }, + then: { + $multiply: [ + { + $add: [ + { + $let: { + vars: { + sellOpenOrder: { + $arrayElemAt: ['$sell.openOrders', 0] + } + }, + in: '$sellOpenOrder.differenceToCancel' + } + }, + 2000 + ] + }, + -10 + ] + }, + else: { + $cond: { + if: { + $eq: ['$sell.difference', null] + }, + then: '$buy.difference', + else: { + $multiply: [{ $add: ['$sell.difference', 1000] }, -10] + } + } + } + } + } + } + } + }, + { + desc: 'buy-difference', + sortByDesc: true, + sortByParam: 'buy-difference', + searchKeyword: 'BTC', + sortField: { + $cond: { + if: { + $eq: ['$buy.difference', null] + }, + then: -999, + else: '$buy.difference' + } + } + }, + { + desc: 'buy-difference', + sortByDesc: false, + sortByParam: 'buy-difference', + searchKeyword: 'BTC', + sortField: { + $cond: { + if: { + $eq: ['$buy.difference', null] + }, + then: 999, + else: '$buy.difference' + } + } + }, + { + desc: 'sell-profit', + sortByDesc: false, + sortByParam: 'sell-profit', + searchKeyword: null, + sortField: { + $cond: { + if: { + $eq: ['$sell.currentProfitPercentage', null] + }, + then: 999, + else: '$sell.currentProfitPercentage' + } + } + }, + { + desc: 'sell-profit', + sortByDesc: true, + sortByParam: 'sell-profit', + searchKeyword: null, + sortField: { + $cond: { + if: { + $eq: ['$sell.currentProfitPercentage', null] + }, + then: -999, + else: '$sell.currentProfitPercentage' + } + } + }, + { + desc: 'alpha', + sortByDesc: true, + sortByParam: 'alpha', + searchKeyword: 'ETH', + sortField: '$symbol' + } + ].forEach(t => { + describe(`sortBy - ${t.desc}`, () => { + beforeEach(async () => { + const { mongo, logger } = require('../../../helpers'); + + mongoMock = mongo; + loggerMock = logger; + + mongoMock.aggregate = jest.fn().mockResolvedValue({ some: 'data' }); + commonHelper = require('../common'); + + result = await commonHelper.getCacheTrailingTradeSymbols( + loggerMock, + t.sortByDesc, + t.sortByParam, + 2, + 10, + t.searchKeyword + ); + }); + + it('triggers mongo.aggregate', () => { + expect(mongoMock.aggregate).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-cache', + [ + { + $match: t.searchKeyword + ? { symbol: { $regex: t.searchKeyword, $options: 'i' } } + : {} + }, + { + $project: { + symbol: '$symbol', + lastCandle: '$lastCandle', + symbolInfo: '$symbolInfo', + symbolConfiguration: '$symbolConfiguration', + baseAssetBalance: '$baseAssetBalance', + quoteAssetBalance: '$quoteAssetBalance', + buy: '$buy', + sell: '$sell', + tradingView: '$tradingView', + overrideData: '$overrideData', + sortField: t.sortField + } + }, + { $sort: { sortField: t.sortByDesc ? -1 : 1 } }, + { $skip: (2 - 1) * 10 }, + { $limit: 10 } + ] + ); + }); + }); + }); + }); + + describe('getCacheTrailingTradeTotalProfitAndLoss', () => { + beforeEach(async () => { + const { mongo, logger } = require('../../../helpers'); + + mongoMock = mongo; + loggerMock = logger; + + mongoMock.aggregate = jest.fn().mockResolvedValue({ some: 'data' }); + commonHelper = require('../common'); + + result = await commonHelper.getCacheTrailingTradeTotalProfitAndLoss( + loggerMock + ); + }); + + it('triggers, mongo.aggregate', () => { + expect(mongoMock.aggregate).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-cache', + [ + { + $group: { + _id: '$quoteAssetBalance.asset', + amount: { + $sum: { + $multiply: ['$baseAssetBalance.total', '$sell.lastBuyPrice'] + } + }, + profit: { $sum: '$sell.currentProfit' }, + estimatedBalance: { $sum: '$baseAssetBalance.estimatedValue' } + } + }, + { + $project: { + asset: '$_id', + amount: '$amount', + profit: '$profit', + estimatedBalance: '$estimatedBalance' + } + } + ] + ); + }); + }); + + describe('getCacheTrailingTradeQuoteEstimates', () => { + beforeEach(async () => { + const { mongo, logger } = require('../../../helpers'); + + mongoMock = mongo; + loggerMock = logger; + + mongoMock.aggregate = jest.fn().mockResolvedValue({ some: 'data' }); + commonHelper = require('../common'); + + result = await commonHelper.getCacheTrailingTradeQuoteEstimates( + loggerMock + ); + }); + + it('triggers, mongo.aggregate', () => { + expect(mongoMock.aggregate).toHaveBeenCalledWith( + loggerMock, + 'trailing-trade-cache', + [ + { + $match: { + 'baseAssetBalance.estimatedValue': { + $gt: 0 + } + } + }, + { + $project: { + baseAsset: '$symbolInfo.baseAsset', + quoteAsset: '$symbolInfo.quoteAsset', + estimatedValue: '$baseAssetBalance.estimatedValue', + tickSize: '$symbolInfo.filterPrice.tickSize' + } + } + ] + ); + }); + }); }); diff --git a/app/cronjob/trailingTradeHelper/__tests__/configuration.test.js b/app/cronjob/trailingTradeHelper/__tests__/configuration.test.js index c4bd1046..a4089855 100644 --- a/app/cronjob/trailingTradeHelper/__tests__/configuration.test.js +++ b/app/cronjob/trailingTradeHelper/__tests__/configuration.test.js @@ -12,51 +12,202 @@ describe('configuration.js', () => { }); describe('saveGlobalConfiguration', () => { - beforeEach(async () => { + beforeEach(() => { cache.hdelall = jest.fn().mockResolvedValue(true); PubSub.publish = jest.fn().mockReturnValue(true); + mongo.findOne = jest.fn().mockResolvedValueOnce({ + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + } + }); mongo.upsertOne = jest.fn().mockResolvedValue(true); mongo.dropIndex = jest.fn().mockResolvedValue(true); mongo.createIndex = jest.fn().mockResolvedValue(true); + }); - result = await configuration.saveGlobalConfiguration(logger, { - myKey: 'value', - botOptions: { - logs: { - deleteAfter: 30 + describe('when old and new configuration are same', () => { + beforeEach(async () => { + result = await configuration.saveGlobalConfiguration(logger, { + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + }, + botOptions: { + logs: { + deleteAfter: 30 + } } - } + }); + }); + + it('triggers cache.hdelall', () => { + expect(cache.hdelall).toHaveBeenCalledWith( + 'trailing-trade-configurations:*' + ); + }); + + it('triggers mongo.upsertOne with expected value', () => { + expect(mongo.upsertOne).toHaveBeenCalledWith( + logger, + 'trailing-trade-common', + { key: 'configuration' }, + { + key: 'configuration', + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + }, + botOptions: { + logs: { + deleteAfter: 30 + } + } + } + ); + }); + + it('do not trigger PubSub.publish', () => { + expect(PubSub.publish).not.toHaveBeenCalledWith( + 'reset-all-websockets', + true + ); }); }); - it('triggers cache.hdelall', () => { - expect(cache.hdelall).toHaveBeenCalledWith( - 'trailing-trade-configurations:*' - ); + describe('when symbols are different', () => { + beforeEach(async () => { + result = await configuration.saveGlobalConfiguration(logger, { + symbols: ['BTCUSDT', 'ETHUSDT', 'BNBUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + }, + botOptions: { + logs: { + deleteAfter: 30 + } + } + }); + }); + + it('triggers PubSub.publish', () => { + expect(PubSub.publish).toHaveBeenCalledWith( + 'reset-all-websockets', + true + ); + }); }); - it('triggers mongo.upsertOne with expected value', () => { - expect(mongo.upsertOne).toHaveBeenCalledWith( - logger, - 'trailing-trade-common', - { key: 'configuration' }, - { - key: 'configuration', - myKey: 'value', + describe('when candles are different', () => { + beforeEach(async () => { + result = await configuration.saveGlobalConfiguration(logger, { + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '15m', + limit: 100 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + }, botOptions: { logs: { deleteAfter: 30 } } - } - ); + }); + }); + + it('triggers PubSub.publish', () => { + expect(PubSub.publish).toHaveBeenCalledWith( + 'reset-all-websockets', + true + ); + }); }); - it('triggers PubSub.publish', () => { - expect(PubSub.publish).toHaveBeenCalledWith( - 'reset-binance-websocket', - true - ); + describe('when ATH restriction candles are different', () => { + beforeEach(async () => { + result = await configuration.saveGlobalConfiguration(logger, { + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '15m', + limit: 60 + } + } + } + }, + botOptions: { + logs: { + deleteAfter: 30 + } + } + }); + }); + + it('triggers PubSub.publish', () => { + expect(PubSub.publish).toHaveBeenCalledWith( + 'reset-all-websockets', + true + ); + }); }); }); @@ -408,6 +559,7 @@ describe('configuration.js', () => { describe('when symbol is not provided', () => { beforeEach(async () => { cache.hdel = jest.fn().mockResolvedValue(true); + PubSub.publish = jest.fn().mockReturnValue(true); mongo.upsertOne = jest.fn().mockResolvedValue(true); result = await configuration.saveSymbolConfiguration(logger); @@ -420,36 +572,269 @@ describe('configuration.js', () => { it('returns expected value', () => { expect(result).toStrictEqual({}); }); + + it('does not trigger reset-symbol-websockets', () => { + expect(PubSub.publish).not.toHaveBeenCalled(); + }); }); describe('when symbol is provided', () => { - beforeEach(async () => { - cache.hdel = jest.fn().mockResolvedValue(true); - mongo.upsertOne = jest.fn().mockResolvedValue(true); + describe('when all configurations are same', () => { + beforeEach(async () => { + PubSub.publish = jest.fn().mockReturnValue(true); + cache.hdel = jest.fn().mockResolvedValue(true); + mongo.findOne = jest.fn().mockResolvedValueOnce({ + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + } + }); + mongo.upsertOne = jest.fn().mockResolvedValue(true); - result = await configuration.saveSymbolConfiguration( - logger, - 'BTCUSDT', - { - myKey: 'value' - } - ); + result = await configuration.saveSymbolConfiguration( + logger, + 'BTCUSDT', + { + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + } + } + ); + }); + + it('triggers mongo.upsertOne', () => { + expect(mongo.upsertOne).toHaveBeenCalledWith( + logger, + 'trailing-trade-symbols', + { key: 'BTCUSDT-configuration' }, + { + key: 'BTCUSDT-configuration', + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + } + } + ); + }); + + it('triggers cache.hdel', () => { + expect(cache.hdel).toHaveBeenCalledWith( + 'trailing-trade-configurations', + 'BTCUSDT' + ); + }); + + it('does not trigger reset-symbol-websockets', () => { + expect(PubSub.publish).not.toHaveBeenCalled(); + }); }); - it('triggers mongo.upsertOne', () => { - expect(mongo.upsertOne).toHaveBeenCalledWith( - logger, - 'trailing-trade-symbols', - { key: 'BTCUSDT-configuration' }, - { key: 'BTCUSDT-configuration', myKey: 'value' } - ); + describe('when candles is different', () => { + beforeEach(async () => { + PubSub.publish = jest.fn().mockReturnValue(true); + cache.hdel = jest.fn().mockResolvedValue(true); + mongo.findOne = jest.fn().mockResolvedValueOnce({ + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '5m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + } + }); + mongo.upsertOne = jest.fn().mockResolvedValue(true); + + result = await configuration.saveSymbolConfiguration( + logger, + 'BTCUSDT', + { + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + } + } + ); + }); + + it('triggers mongo.upsertOne', () => { + expect(mongo.upsertOne).toHaveBeenCalledWith( + logger, + 'trailing-trade-symbols', + { key: 'BTCUSDT-configuration' }, + { + key: 'BTCUSDT-configuration', + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + } + } + ); + }); + + it('triggers cache.hdel', () => { + expect(cache.hdel).toHaveBeenCalledWith( + 'trailing-trade-configurations', + 'BTCUSDT' + ); + }); + + it('triggers reset-symbol-websockets', () => { + expect(PubSub.publish).toHaveBeenCalledWith( + 'reset-symbol-websockets', + 'BTCUSDT' + ); + }); }); - it('triggers cache.hdel', () => { - expect(cache.hdel).toHaveBeenCalledWith( - 'trailing-trade-configurations', - 'BTCUSDT' - ); + describe('when athRestriction.candles is different', () => { + beforeEach(async () => { + PubSub.publish = jest.fn().mockReturnValue(true); + cache.hdel = jest.fn().mockResolvedValue(true); + mongo.findOne = jest.fn().mockResolvedValueOnce({ + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '30m', + limit: 30 + } + } + } + } + }); + mongo.upsertOne = jest.fn().mockResolvedValue(true); + + result = await configuration.saveSymbolConfiguration( + logger, + 'BTCUSDT', + { + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + } + } + ); + }); + + it('triggers mongo.upsertOne', () => { + expect(mongo.upsertOne).toHaveBeenCalledWith( + logger, + 'trailing-trade-symbols', + { key: 'BTCUSDT-configuration' }, + { + key: 'BTCUSDT-configuration', + symbols: ['BTCUSDT', 'ETHUSDT'], + candles: { + interval: '1m', + limit: 10 + }, + buy: { + athRestriction: { + candles: { + candles: { + interval: '1d', + limit: 30 + } + } + } + } + } + ); + }); + + it('triggers cache.hdel', () => { + expect(cache.hdel).toHaveBeenCalledWith( + 'trailing-trade-configurations', + 'BTCUSDT' + ); + }); + + it('triggers reset-symbol-websockets', () => { + expect(PubSub.publish).toHaveBeenCalledWith( + 'reset-symbol-websockets', + 'BTCUSDT' + ); + }); }); }); }); diff --git a/app/cronjob/trailingTradeHelper/__tests__/order.test.js b/app/cronjob/trailingTradeHelper/__tests__/order.test.js index 9e31bea9..1f6258d6 100644 --- a/app/cronjob/trailingTradeHelper/__tests__/order.test.js +++ b/app/cronjob/trailingTradeHelper/__tests__/order.test.js @@ -194,4 +194,74 @@ describe('order.js', () => { ); }); }); + + describe('getGridTradeLastOrder', () => { + describe('when order is avilable', () => { + beforeEach(async () => { + mongo.findOne = jest.fn().mockResolvedValue({ + order: { + id: 'order-123' + } + }); + + result = await order.getGridTradeLastOrder(logger, 'BTCUSDT', 'buy'); + }); + + it('triggers mongo.findOne', () => { + expect(mongo.findOne).toHaveBeenCalledWith( + logger, + 'trailing-trade-grid-trade-orders', + { key: 'BTCUSDT-grid-trade-last-buy-order' } + ); + }); + + it('returns expected result', () => { + expect(result).toStrictEqual({ + id: 'order-123' + }); + }); + }); + + describe('when order is not available', () => { + beforeEach(async () => { + mongo.findOne = jest.fn().mockResolvedValue(null); + + result = await order.getGridTradeLastOrder(logger, 'BTCUSDT', 'buy'); + }); + + it('triggers mongo.findOne', () => { + expect(mongo.findOne).toHaveBeenCalledWith( + logger, + 'trailing-trade-grid-trade-orders', + { key: 'BTCUSDT-grid-trade-last-buy-order' } + ); + }); + + it('returns expected result', () => { + expect(result).toStrictEqual({}); + }); + }); + }); + + describe('updateGridTradeLastOrder', () => { + beforeEach(async () => { + mongo.upsertOne = jest.fn().mockResolvedValue(true); + + result = await order.updateGridTradeLastOrder(logger, 'BTCUSDT', 'buy', { + id: 'new-order' + }); + }); + + it('triggers mongo.upsertOne', () => { + expect(mongo.upsertOne).toHaveBeenCalledWith( + logger, + 'trailing-trade-grid-trade-orders', + { key: 'BTCUSDT-grid-trade-last-buy-order' }, + { + key: 'BTCUSDT-grid-trade-last-buy-order', + order: { id: 'new-order' } + } + ); + }); + }); }); diff --git a/app/cronjob/trailingTradeHelper/common.js b/app/cronjob/trailingTradeHelper/common.js index 85e717d8..9e19b774 100644 --- a/app/cronjob/trailingTradeHelper/common.js +++ b/app/cronjob/trailingTradeHelper/common.js @@ -16,9 +16,8 @@ const isValidCachedExchangeSymbols = exchangeSymbols => * If not cached, retrieve exchange info from API and cache it. * * @param {*} logger - * @param {*} globalConfiguration */ -const cacheExchangeSymbols = async (logger, _globalConfiguration) => { +const cacheExchangeSymbols = async logger => { const cachedExchangeSymbols = JSON.parse(await cache.hget('trailing-trade-common', 'exchange-symbols')) || {}; @@ -78,12 +77,16 @@ const cacheExchangeSymbols = async (logger, _globalConfiguration) => { logger.info({ exchangeSymbols }, 'Saved exchange symbols to cache'); }; +const getCachedExchangeSymbols = async _logger => + JSON.parse(await cache.hget('trailing-trade-common', 'exchange-symbols')) || + {}; + /** - * Add estimatedBTC and canDustTransfer flags to balances + * Add estimatedBTC and canDustTransfer flags to balance * - Leave this function for future reference * - * @param {*} logger - * @param {*} accountInfo + * @param {*} _logger + * @param {*} rawAccountInfo * @returns */ const extendBalancesWithDustTransfer = async (_logger, rawAccountInfo) => { @@ -330,7 +333,7 @@ const lockSymbol = async (logger, symbol, ttl = 5) => { /** * Check if symbol is locked * - * @param {*} _logger + * @param {*} logger * @param {*} symbol * @returns */ @@ -433,7 +436,7 @@ const isExceedAPILimit = logger => { /** * Get override data for Symbol * - * @param {*} logger + * @param {*} _logger * @param {*} symbol * @returns */ @@ -449,7 +452,7 @@ const getOverrideDataForSymbol = async (_logger, symbol) => { /** * Remove override data for Symbol * - * @param {*} _logger + * @param {*} logger * @param {*} symbol * @returns */ @@ -462,7 +465,7 @@ const removeOverrideDataForSymbol = async (logger, symbol) => { /** * Get override data for Indicator * - * @param {*} logger + * @param {*} _logger * @param {*} key * @returns */ @@ -765,7 +768,7 @@ const saveOverrideAction = async ( overrideReason ) => { logger.info( - { overrideData, overrideReason, saveLog: true }, + { symbol, overrideData, overrideReason, saveLog: true }, `The override action is saved. Reason: ${overrideReason}` ); @@ -797,7 +800,7 @@ const saveOverrideAction = async ( * Save override action for indicator * * @param {*} logger - * @param {*} symbol + * @param {*} type * @param {*} overrideData * @param {*} overrideReason */ @@ -831,8 +834,256 @@ const saveOverrideIndicatorAction = async ( } }; +/** + * Save or update symbol candle based on time + * + * @param {*} logger + * @param collectionName + * @param candle + */ +const saveCandle = async (logger, collectionName, candle) => { + const { key, interval, time } = candle; + await mongo.upsertOne( + logger, + collectionName, + { + key, + time, + interval + }, + candle + ); +}; + +/** + * Update account info with new one + * + * @param {*} logger + * @param balances + * @param lastAccountUpdate + */ +const updateAccountInfo = async (logger, balances, lastAccountUpdate) => { + logger.info({ balances }, 'Updating account balances'); + const accountInfo = await getAccountInfo(logger); + + const mergedBalances = _.merge( + _.keyBy(accountInfo.balances, 'asset'), + _.keyBy(balances, 'asset') + ); + accountInfo.balances = _.reduce( + _.values(mergedBalances), + (acc, b) => { + const balance = b; + if (+balance.free > 0 || +balance.locked > 0) { + acc.push(balance); + } + + return acc; + }, + [] + ); + + // set updateTime manually because we are updating account info from websocket + accountInfo.updateTime = lastAccountUpdate; + + await cache.hset( + 'trailing-trade-common', + 'account-info', + JSON.stringify(accountInfo) + ); + + return accountInfo; +}; + +const getCacheTrailingTradeSymbols = async ( + logger, + sortByDesc, + sortByParam, + page, + symbolsPerPage, + searchKeyword +) => { + const match = {}; + + if (searchKeyword) { + match.symbol = { + $regex: searchKeyword, + $options: 'i' + }; + } + + const sortBy = sortByParam || 'default'; + const sortDirection = sortByDesc === true ? -1 : 1; + + logger.info({ sortBy, sortDirection }, 'latest'); + + let sortField = { + $cond: { + if: { $gt: [{ $size: '$buy.openOrders' }, 0] }, + then: { + $multiply: [ + { + $add: [ + { + $let: { + vars: { + buyOpenOrder: { + $arrayElemAt: ['$buy.openOrders', 0] + } + }, + in: '$buyOpenOrder.differenceToCancel' + } + }, + 3000 + ] + }, + -10 + ] + }, + else: { + $cond: { + if: { $gt: [{ $size: '$sell.openOrders' }, 0] }, + then: { + $multiply: [ + { + $add: [ + { + $let: { + vars: { + sellOpenOrder: { + $arrayElemAt: ['$sell.openOrders', 0] + } + }, + in: '$sellOpenOrder.differenceToCancel' + } + }, + 2000 + ] + }, + -10 + ] + }, + else: { + $cond: { + if: { + $eq: ['$sell.difference', null] + }, + then: '$buy.difference', + else: { + $multiply: [{ $add: ['$sell.difference', 1000] }, -10] + } + } + } + } + } + } + }; + + if (sortBy === 'buy-difference') { + sortField = { + $cond: { + if: { + $eq: ['$buy.difference', null] + }, + then: sortByDesc ? -999 : 999, + else: '$buy.difference' + } + }; + } + + if (sortBy === 'sell-profit') { + sortField = { + $cond: { + if: { + $eq: ['$sell.currentProfitPercentage', null] + }, + then: sortByDesc ? -999 : 999, + else: '$sell.currentProfitPercentage' + } + }; + } + + if (sortBy === 'alpha') { + sortField = '$symbol'; + } + + const trailingTradeCacheQuery = [ + { + $match: match + }, + { + $project: { + symbol: '$symbol', + lastCandle: '$lastCandle', + symbolInfo: '$symbolInfo', + symbolConfiguration: '$symbolConfiguration', + baseAssetBalance: '$baseAssetBalance', + quoteAssetBalance: '$quoteAssetBalance', + buy: '$buy', + sell: '$sell', + tradingView: '$tradingView', + overrideData: '$overrideData', + sortField + } + }, + { $sort: { sortField: sortDirection } }, + { $skip: (page - 1) * symbolsPerPage }, + { $limit: symbolsPerPage } + ]; + + return mongo.aggregate( + logger, + 'trailing-trade-cache', + trailingTradeCacheQuery + ); +}; + +const getCacheTrailingTradeTotalProfitAndLoss = logger => + mongo.aggregate(logger, 'trailing-trade-cache', [ + { + $group: { + _id: '$quoteAssetBalance.asset', + amount: { + $sum: { + $multiply: ['$baseAssetBalance.total', '$sell.lastBuyPrice'] + } + }, + profit: { $sum: '$sell.currentProfit' }, + estimatedBalance: { $sum: '$baseAssetBalance.estimatedValue' } + } + }, + { + $project: { + asset: '$_id', + amount: '$amount', + profit: '$profit', + estimatedBalance: '$estimatedBalance' + } + } + ]); + +const getCacheTrailingTradeQuoteEstimates = logger => + mongo.aggregate(logger, 'trailing-trade-cache', [ + { + $match: { + 'baseAssetBalance.estimatedValue': { + $gt: 0 + } + } + }, + { + $project: { + baseAsset: '$symbolInfo.baseAsset', + quoteAsset: '$symbolInfo.quoteAsset', + estimatedValue: '$baseAssetBalance.estimatedValue', + tickSize: '$symbolInfo.filterPrice.tickSize' + } + } + ]); + module.exports = { cacheExchangeSymbols, + getCachedExchangeSymbols, getAccountInfoFromAPI, getAccountInfo, extendBalancesWithDustTransfer, @@ -863,5 +1114,10 @@ module.exports = { getNumberOfOpenTrades, saveOrderStats, saveOverrideAction, - saveOverrideIndicatorAction + saveOverrideIndicatorAction, + saveCandle, + updateAccountInfo, + getCacheTrailingTradeSymbols, + getCacheTrailingTradeTotalProfitAndLoss, + getCacheTrailingTradeQuoteEstimates }; diff --git a/app/cronjob/trailingTradeHelper/configuration.js b/app/cronjob/trailingTradeHelper/configuration.js index dd30eb40..fa93b5e2 100644 --- a/app/cronjob/trailingTradeHelper/configuration.js +++ b/app/cronjob/trailingTradeHelper/configuration.js @@ -51,6 +51,15 @@ const reconfigureIndex = async (logger, configuration) => { * @param {*} configuration */ const saveGlobalConfiguration = async (logger, configuration) => { + // get old configuration before saving the new one to compare + const oldConfiguration = await mongo.findOne( + logger, + 'trailing-trade-common', + { + key: 'configuration' + } + ); + // Save to cache for watching changes. const result = await mongo.upsertOne( logger, @@ -67,7 +76,24 @@ const saveGlobalConfiguration = async (logger, configuration) => { await cache.hdelall('trailing-trade-configurations:*'); await reconfigureIndex(logger, configuration); - PubSub.publish('reset-binance-websocket', true); + + // reset all websockets only when symbols, candles or ath candles are changed + if ( + _.isEqual( + _.get(oldConfiguration, 'symbols', []), + _.get(configuration, 'symbols', []) + ) === false || + _.isEqual( + _.get(oldConfiguration, 'candles', {}), + _.get(configuration, 'candles', {}) + ) === false || + _.isEqual( + _.get(oldConfiguration, ['buy', 'athRestriction', 'candles'], {}), + _.get(configuration, ['buy', 'athRestriction', 'candles'], {}) + ) === false + ) { + PubSub.publish('reset-all-websockets', true); + } return result; }; @@ -174,6 +200,8 @@ const saveSymbolConfiguration = async ( return {}; } + const oldConfiguration = await getSymbolConfiguration(logger, symbol); + const result = await mongo.upsertOne( logger, 'trailing-trade-symbols', @@ -188,6 +216,20 @@ const saveSymbolConfiguration = async ( await cache.hdel('trailing-trade-configurations', symbol); + // reset symbol websockets only when candles or ath candles are changed + if ( + _.isEqual( + _.get(oldConfiguration, 'candles', {}), + _.get(configuration, 'candles', {}) + ) === false || + _.isEqual( + _.get(oldConfiguration, ['buy', 'athRestriction', 'candles'], {}), + _.get(configuration, ['buy', 'athRestriction', 'candles'], {}) + ) === false + ) { + PubSub.publish('reset-symbol-websockets', symbol); + } + return result; }; @@ -451,6 +493,9 @@ const deleteSymbolConfiguration = async (logger, symbol) => { }); await cache.hdel('trailing-trade-configurations', symbol); + + PubSub.publish('reset-symbol-websockets', symbol); + return result; }; diff --git a/app/cronjob/trailingTradeHelper/order.js b/app/cronjob/trailingTradeHelper/order.js index d2a588a9..53e504d6 100644 --- a/app/cronjob/trailingTradeHelper/order.js +++ b/app/cronjob/trailingTradeHelper/order.js @@ -65,6 +65,7 @@ const getManualOrders = async (logger, symbol) => * Get manual trade order * @param {*} logger * @param {*} symbol + * @param {*} orderId * @returns */ const getManualOrder = async (logger, symbol, orderId) => { @@ -113,6 +114,47 @@ const deleteManualOrder = async (logger, symbol, orderId) => { }); }; +/** + * Retrieve last grid order from cache + * + * @param {*} logger + * @param {*} symbol + * @param {*} side + * @returns + */ +const getGridTradeLastOrder = async (logger, symbol, side) => { + const lastOrder = + (await getGridTradeOrder( + logger, + `${symbol}-grid-trade-last-${side}-order` + )) || {}; + + logger.info( + { lastOrder }, + `Retrieved grid trade last ${side} order from cache` + ); + + return lastOrder; +}; + +/** + * Update grid trade order + * + * @param {*} logger + * @param {*} symbol + * @param {*} side + * @param {*} newOrder + */ +const updateGridTradeLastOrder = async (logger, symbol, side, newOrder) => { + await saveGridTradeOrder( + logger, + `${symbol}-grid-trade-last-${side}-order`, + newOrder + ); + + logger.info(`Updated grid trade last ${side} order to cache`); +}; + module.exports = { getGridTradeOrder, saveGridTradeOrder, @@ -120,5 +162,7 @@ module.exports = { getManualOrders, getManualOrder, saveManualOrder, - deleteManualOrder + deleteManualOrder, + getGridTradeLastOrder, + updateGridTradeLastOrder }; diff --git a/app/cronjob/trailingTradeIndicator.js b/app/cronjob/trailingTradeIndicator.js index 5a6205c6..ae78628c 100644 --- a/app/cronjob/trailingTradeIndicator.js +++ b/app/cronjob/trailingTradeIndicator.js @@ -14,9 +14,6 @@ const { getSymbolConfiguration, getSymbolInfo, getOverrideAction, - getAccountInfo, - getIndicators, - getOpenOrders, executeDustTransfer, getClosedTrades, getOrderStats, @@ -37,9 +34,6 @@ const execute = async logger => { symbol: null, symbolConfiguration: {}, symbolInfo: {}, - accountInfo: {}, - indicators: {}, - openOrders: [], overrideParams: {}, quoteAssetStats: {}, tradingView: {}, @@ -85,21 +79,6 @@ const execute = async logger => { stepName: 'get-symbol-info', stepFunc: getSymbolInfo }, - { - stepName: 'get-account-info', - stepFunc: getAccountInfo - }, - { - stepName: 'get-indicators', - stepFunc: getIndicators - }, - { - // Note that open orders for all symbols cannot exceed 40 request per minute. - // Hence, this must be executed every 2 seconds. - // After placing buy/sell orders, the bot will retrieve symbol open orders which can request every second. - stepName: 'get-open-orders', - stepFunc: getOpenOrders - }, { stepName: 'get-closed-trades', stepFunc: getClosedTrades diff --git a/app/cronjob/trailingTradeIndicator/step/__tests__/get-account-info.test.js b/app/cronjob/trailingTradeIndicator/step/__tests__/get-account-info.test.js index 73afc49f..7bc03e76 100644 --- a/app/cronjob/trailingTradeIndicator/step/__tests__/get-account-info.test.js +++ b/app/cronjob/trailingTradeIndicator/step/__tests__/get-account-info.test.js @@ -10,7 +10,7 @@ describe('get-account-info.js', () => { let step; let result; - let mockGetAccountInfoFromAPI; + let mockGetAccountInfo; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -36,13 +36,13 @@ describe('get-account-info.js', () => { cacheMock.hset = jest.fn().mockResolvedValue(true); PubSub.publish = jest.fn().mockResolvedValue(true); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info', balances: [{ asset: 'BTC' }, { asset: 'XRP' }, { asset: 'ETH' }] }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI + getAccountInfo: mockGetAccountInfo })); step = require('../get-account-info'); @@ -74,7 +74,7 @@ describe('get-account-info.js', () => { it('triggers PubSub.publish', () => { expect(PubSubMock.publish).toHaveBeenCalledWith( - 'reset-binance-websocket', + 'reset-all-websockets', true ); }); @@ -110,13 +110,13 @@ describe('get-account-info.js', () => { cacheMock.hset = jest.fn().mockResolvedValue(true); PubSub.publish = jest.fn().mockResolvedValue(true); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info', balances: [{ asset: 'BTC' }, { asset: 'XRP' }, { asset: 'ETH' }] }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI + getAccountInfo: mockGetAccountInfo })); step = require('../get-account-info'); @@ -173,13 +173,13 @@ describe('get-account-info.js', () => { cacheMock.hset = jest.fn().mockResolvedValue(true); PubSub.publish = jest.fn().mockResolvedValue(true); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info', balances: [{ asset: 'BTC' }, { asset: 'XRP' }, { asset: 'ETH' }] }); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI + getAccountInfo: mockGetAccountInfo })); step = require('../get-account-info'); @@ -237,7 +237,7 @@ describe('get-account-info.js', () => { cacheMock.hset = jest.fn().mockResolvedValue(true); PubSub.publish = jest.fn().mockResolvedValue(true); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info', balances: [{ asset: 'BTC' }, { asset: 'XRP' }, { asset: 'ETH' }] }); @@ -249,7 +249,7 @@ describe('get-account-info.js', () => { ); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI + getAccountInfo: mockGetAccountInfo })); step = require('../get-account-info'); @@ -292,7 +292,7 @@ describe('get-account-info.js', () => { cacheMock.hset = jest.fn().mockResolvedValue(true); PubSub.publish = jest.fn().mockResolvedValue(true); - mockGetAccountInfoFromAPI = jest.fn().mockResolvedValue({ + mockGetAccountInfo = jest.fn().mockResolvedValue({ account: 'info', balances: [{ asset: 'BTC' }, { asset: 'XRP' }, { asset: 'ETH' }] }); @@ -304,7 +304,7 @@ describe('get-account-info.js', () => { ); jest.mock('../../../trailingTradeHelper/common', () => ({ - getAccountInfoFromAPI: mockGetAccountInfoFromAPI + getAccountInfo: mockGetAccountInfo })); step = require('../get-account-info'); diff --git a/app/cronjob/trailingTradeIndicator/step/__tests__/save-data-to-cache.test.js b/app/cronjob/trailingTradeIndicator/step/__tests__/save-data-to-cache.test.js index 64a0c9a4..706b87c6 100644 --- a/app/cronjob/trailingTradeIndicator/step/__tests__/save-data-to-cache.test.js +++ b/app/cronjob/trailingTradeIndicator/step/__tests__/save-data-to-cache.test.js @@ -29,16 +29,6 @@ describe('save-data-to-cache.js', () => { await step.execute(logger, rawData); }); - it('triggers cache.hset for symbol indicator data', () => { - expect(cache.hset).toHaveBeenCalledWith( - 'trailing-trade-symbols', - 'BTCUSDT-indicator-data', - JSON.stringify({ - some: 'value' - }) - ); - }); - it('triggers cache.hset for trailing trade quote assets data', () => { expect(cache.hset).toHaveBeenCalledWith( 'trailing-trade-closed-trades', diff --git a/app/cronjob/trailingTradeIndicator/step/get-account-info.js b/app/cronjob/trailingTradeIndicator/step/get-account-info.js index 9a3ddf77..f50ac6c4 100644 --- a/app/cronjob/trailingTradeIndicator/step/get-account-info.js +++ b/app/cronjob/trailingTradeIndicator/step/get-account-info.js @@ -3,7 +3,7 @@ const moment = require('moment'); const { cache, PubSub } = require('../../../helpers'); -const { getAccountInfoFromAPI } = require('../../trailingTradeHelper/common'); +const { getAccountInfo } = require('../../trailingTradeHelper/common'); const isAccountInfoChanged = async ( logger, @@ -55,7 +55,7 @@ const execute = async (logger, rawData) => { const oldAccountInfo = JSON.parse(await cache.hget('trailing-trade-common', 'account-info')) || {}; - const accountInfo = await getAccountInfoFromAPI(logger); + const accountInfo = await getAccountInfo(logger); await cache.hset( 'trailing-trade-common', @@ -65,7 +65,7 @@ const execute = async (logger, rawData) => { // Determine to reset binance websocket if (await isAccountInfoChanged(logger, { oldAccountInfo, accountInfo })) { - PubSub.publish('reset-binance-websocket', true); + PubSub.publish('reset-all-websockets', true); } data.accountInfo = accountInfo; diff --git a/app/cronjob/trailingTradeIndicator/step/save-data-to-cache.js b/app/cronjob/trailingTradeIndicator/step/save-data-to-cache.js index 24f5c31b..afceed92 100644 --- a/app/cronjob/trailingTradeIndicator/step/save-data-to-cache.js +++ b/app/cronjob/trailingTradeIndicator/step/save-data-to-cache.js @@ -3,19 +3,13 @@ const { cache } = require('../../../helpers'); /** * Save data to cache * - * @param {*} logger + * @param {*} _logger * @param {*} rawData */ const execute = async (_logger, rawData) => { const data = rawData; - const { symbol, symbolInfo, indicators, closedTrades } = data; - - cache.hset( - 'trailing-trade-symbols', - `${symbol}-indicator-data`, - JSON.stringify(indicators) - ); + const { symbolInfo, closedTrades } = data; const { quoteAsset } = symbolInfo; diff --git a/app/frontend/webserver/handlers/__tests__/grid-trade-logs-export.test.js b/app/frontend/webserver/handlers/__tests__/grid-trade-logs-export.test.js index f33cb7bc..33520b71 100644 --- a/app/frontend/webserver/handlers/__tests__/grid-trade-logs-export.test.js +++ b/app/frontend/webserver/handlers/__tests__/grid-trade-logs-export.test.js @@ -1,4 +1,8 @@ /* eslint-disable global-require */ +const { tmpdir: tmpDirectory } = require('os'); +const { sep: directorySeparator } = require('path'); +const _ = require('lodash'); + describe('webserver/handlers/grid-trade-logs-export', () => { let loggerMock; let mongoMock; @@ -129,8 +133,12 @@ describe('webserver/handlers/grid-trade-logs-export', () => { }); it('return data', () => { + const tempDirLocation = _.escapeRegExp( + `${tmpDirectory()}${directorySeparator}` + ); + expect(rsDownload).toHaveBeenCalledWith( - expect.stringMatching('/tmp/(.+).json') + expect.stringMatching(`${tempDirLocation}(.+).json`) ); }); }); @@ -180,8 +188,12 @@ describe('webserver/handlers/grid-trade-logs-export', () => { }); it('return data', () => { + const tempDirLocation = _.escapeRegExp( + `${tmpDirectory()}${directorySeparator}` + ); + expect(rsDownload).toHaveBeenCalledWith( - expect.stringMatching('/tmp/(.+).json') + expect.stringMatching(`${tempDirLocation}(.+).json`) ); }); }); diff --git a/app/frontend/webserver/handlers/grid-trade-logs-export.js b/app/frontend/webserver/handlers/grid-trade-logs-export.js index c4aa8f05..83dc9882 100644 --- a/app/frontend/webserver/handlers/grid-trade-logs-export.js +++ b/app/frontend/webserver/handlers/grid-trade-logs-export.js @@ -1,6 +1,8 @@ const { v4: uuidv4 } = require('uuid'); const fs = require('fs'); +const { tmpdir: tmpDirectory } = require('os'); +const { sep: directorySeparator } = require('path'); const { verifyAuthenticated } = require('../../../cronjob/trailingTradeHelper/common'); @@ -42,7 +44,7 @@ const handleGridTradeLogsExport = async (funcLogger, app) => { const rows = await mongo.findAll(logger, 'trailing-trade-logs', match, { sort: { loggedAt: -1 } }); - const filePath = `/tmp/${uuidv4()}.json`; + const filePath = `${tmpDirectory()}${directorySeparator}${uuidv4()}.json`; fs.writeFileSync(filePath, JSON.stringify(rows)); return res.download(filePath); diff --git a/app/frontend/websocket/configure.js b/app/frontend/websocket/configure.js index 4fae995e..b051e05b 100644 --- a/app/frontend/websocket/configure.js +++ b/app/frontend/websocket/configure.js @@ -22,7 +22,8 @@ const { handleManualTradeAllSymbols, handleCancelOrder, handleDustTransferGet, - handleDustTransferExecute + handleDustTransferExecute, + handleExchangeSymbolsGet } = require('./handlers'); const handleWarning = (logger, ws, message) => { @@ -95,7 +96,8 @@ const configureWebSocket = async (server, funcLogger, { loginLimiter }) => { 'manual-trade-all-symbols': handleManualTradeAllSymbols, 'cancel-order': handleCancelOrder, 'dust-transfer-get': handleDustTransferGet, - 'dust-transfer-execute': handleDustTransferExecute + 'dust-transfer-execute': handleDustTransferExecute, + 'exchange-symbols-get': handleExchangeSymbolsGet }; if (commandMaps[payload.command] === undefined) { diff --git a/app/frontend/websocket/handlers/__tests__/cancel-order.test.js b/app/frontend/websocket/handlers/__tests__/cancel-order.test.js index 99de075a..a45b9b54 100644 --- a/app/frontend/websocket/handlers/__tests__/cancel-order.test.js +++ b/app/frontend/websocket/handlers/__tests__/cancel-order.test.js @@ -6,6 +6,7 @@ describe('cancel-order.js', () => { let loggerMock; let mockSaveOverrideAction; + let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -17,10 +18,15 @@ describe('cancel-order.js', () => { }; mockSaveOverrideAction = jest.fn().mockResolvedValue(true); + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); jest.mock('../../../../cronjob/trailingTradeHelper/common', () => ({ saveOverrideAction: mockSaveOverrideAction })); + + jest.mock('../../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); }); beforeEach(async () => { @@ -53,6 +59,13 @@ describe('cancel-order.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify({ diff --git a/app/frontend/websocket/handlers/__tests__/exchange-symbols-get.test.js b/app/frontend/websocket/handlers/__tests__/exchange-symbols-get.test.js new file mode 100644 index 00000000..091f0e85 --- /dev/null +++ b/app/frontend/websocket/handlers/__tests__/exchange-symbols-get.test.js @@ -0,0 +1,81 @@ +/* eslint-disable global-require */ +describe('exchange-symbols-get.js', () => { + let mockWebSocketServer; + let mockWebSocketServerWebSocketSend; + + let loggerMock; + let cacheMock; + + beforeEach(() => { + jest.clearAllMocks().resetModules(); + + mockWebSocketServerWebSocketSend = jest.fn().mockResolvedValue(true); + + mockWebSocketServer = { + send: mockWebSocketServerWebSocketSend + }; + + const { cache, logger } = require('../../../../helpers'); + + cacheMock = cache; + loggerMock = logger; + }); + + describe('when got cache successfully', () => { + beforeEach(async () => { + cacheMock.hget = jest.fn().mockResolvedValue( + JSON.stringify({ + some: 'data' + }) + ); + + const { handleExchangeSymbolsGet } = require('../exchange-symbols-get'); + await handleExchangeSymbolsGet(loggerMock, mockWebSocketServer, {}); + }); + + it('triggers cache.hget', () => { + expect(cacheMock.hget).toHaveBeenCalledWith( + 'trailing-trade-common', + 'exchange-symbols' + ); + }); + + it('returns expected value', () => { + expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( + JSON.stringify({ + result: true, + type: 'exchange-symbols-get-result', + exchangeSymbols: { + some: 'data' + } + }) + ); + }); + }); + + describe('when failed to get cache', () => { + beforeEach(async () => { + cacheMock.hget = jest.fn().mockResolvedValue(null); + + const { handleExchangeSymbolsGet } = require('../exchange-symbols-get'); + await handleExchangeSymbolsGet(loggerMock, mockWebSocketServer, {}); + }); + + it('triggers cache.hget', () => { + expect(cacheMock.hget).toHaveBeenCalledWith( + 'trailing-trade-common', + 'exchange-symbols' + ); + }); + + it('returns expected value', () => { + expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( + JSON.stringify({ + result: true, + type: 'exchange-symbols-get-result', + exchangeSymbols: {} + }) + ); + }); + }); +}); diff --git a/app/frontend/websocket/handlers/__tests__/fixtures/latest-stats-authenticated.json b/app/frontend/websocket/handlers/__tests__/fixtures/latest-stats-authenticated.json index c5075b57..65e070fc 100644 --- a/app/frontend/websocket/handlers/__tests__/fixtures/latest-stats-authenticated.json +++ b/app/frontend/websocket/handlers/__tests__/fixtures/latest-stats-authenticated.json @@ -3,17 +3,42 @@ "type": "latest", "isAuthenticated": true, "botOptions": { - "authentication": { "lockList": true, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": false, "triggerAfter": 20 }, - "orderLimit": { "enabled": true, "maxBuyOpenOrders": 3, "maxOpenTrades": 5 } + "authentication": { + "lockList": true, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": false, + "triggerAfter": 20 + }, + "orderLimit": { + "enabled": true, + "maxBuyOpenOrders": 3, + "maxOpenTrades": 5 + } }, "configuration": { "enabled": true, "type": "i-am-global", - "candles": { "interval": "15m" }, + "candles": { + "interval": "15m" + }, + "symbols": [ + "BTCUSDT", + "BNBUSDT", + "ETHBUSD", + "BTCBUSD", + "LTCBUSD" + ], "botOptions": { - "authentication": { "lockList": true, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": false, "triggerAfter": 20 }, + "authentication": { + "lockList": true, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": false, + "triggerAfter": 20 + }, "orderLimit": { "enabled": true, "maxBuyOpenOrders": 3, @@ -23,7 +48,7 @@ "sell": {} }, "common": { - "version": "0.0.78", + "version": "0.0.85", "gitHash": "some-hash", "accountInfo": { "makerCommission": 0, @@ -36,109 +61,89 @@ "updateTime": 1630151787045, "accountType": "SPOT", "balances": [ - { "asset": "BNB", "free": "0.01000000", "locked": "0.04000000" }, - { "asset": "BTC", "free": "0.00000100", "locked": "0.00000000" }, - { "asset": "BUSD", "free": "9904.79690824", "locked": "41.96541778" }, - { "asset": "ETH", "free": "0.10001000", "locked": "99.90687000" }, - { "asset": "LTC", "free": "499.75398000", "locked": "0.00000000" }, - { "asset": "TRX", "free": "500000.00000000", "locked": "0.00000000" }, - { "asset": "USDT", "free": "379068.42127337", "locked": "0.00000000" }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } + { + "asset": "BNB", + "free": "0.01000000", + "locked": "0.04000000", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "BTC", + "free": "0.00000100", + "locked": "0.00000000", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "BUSD", + "free": "9904.79690824", + "locked": "41.96541778", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "ETH", + "free": "0.10001000", + "locked": "99.90687000", + "quote": "USDT", + "estimate": "1574.50", + "tickSize": "0.01000000" + }, + { + "asset": "LTC", + "free": "499.75398000", + "locked": "0.00000000", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "TRX", + "free": "500000.00000000", + "locked": "0.00000000", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "USDT", + "free": "379068.42127337", + "locked": "0.00000000", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "XRP", + "free": "50000.00000000", + "locked": "0.00000000", + "quote": null, + "estimate": null, + "tickSize": null + } ], - "permissions": ["SPOT"] + "permissions": [ + "SPOT" + ] }, - "exchangeSymbols": { - "BNBBUSD": { - "symbol": "BNBBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 - }, - "BTCBUSD": { - "symbol": "BTCBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 - }, - "ETHBUSD": { - "symbol": "ETHBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 - }, - "LTCBUSD": { - "symbol": "LTCBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 - }, - "TRXBUSD": { - "symbol": "TRXBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 + "apiInfo": { + "spot": { + "usedWeight1m": "60" }, - "XRPBUSD": { - "symbol": "XRPBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 - }, - "BNBUSDT": { - "symbol": "BNBUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "BTCUSDT": { - "symbol": "BTCUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "ETHUSDT": { - "symbol": "ETHUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "LTCUSDT": { - "symbol": "LTCUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "TRXUSDT": { - "symbol": "TRXUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "XRPUSDT": { - "symbol": "XRPUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "BNBBTC": { - "symbol": "BNBBTC", - "quoteAsset": "BTC", - "minNotional": 0.0001 - }, - "ETHBTC": { - "symbol": "ETHBTC", - "quoteAsset": "BTC", - "minNotional": 0.0001 - }, - "LTCBTC": { - "symbol": "LTCBTC", - "quoteAsset": "BTC", - "minNotional": 0.0001 - }, - "TRXBTC": { - "symbol": "TRXBTC", - "quoteAsset": "BTC", - "minNotional": 0.0001 - }, - "XRPBTC": { - "symbol": "XRPBTC", - "quoteAsset": "BTC", - "minNotional": 0.0001 - }, - "LTCBNB": { "symbol": "LTCBNB", "quoteAsset": "BNB", "minNotional": 0.1 }, - "TRXBNB": { "symbol": "TRXBNB", "quoteAsset": "BNB", "minNotional": 0.1 }, - "XRPBNB": { "symbol": "XRPBNB", "quoteAsset": "BNB", "minNotional": 0.1 } + "futures": {} + }, + "closedTradesSetting": { + "loadedPeriod": "a", + "selectedPeriod": "a" + }, + "orderStats": { + "numberOfOpenTrades": 6, + "numberOfBuyOpenOrders": 3 }, - "apiInfo": { "spot": { "usedWeight1m": "60" }, "futures": {} }, - "closedTradesSetting": { "loadedPeriod": "a", "selectedPeriod": "a" }, "closedTrades": [ { "_id": "USDT", @@ -169,7 +174,10 @@ "profitPercentage": 1601.995003799773 } ], - "orderStats": { "numberOfOpenTrades": 6, "numberOfBuyOpenOrders": 3 } + "totalProfitAndLoss": [], + "streamsCount": 6, + "symbolsCount": 5, + "totalPages": 1 }, "stats": { "symbols": [ @@ -186,86 +194,34 @@ "symbol": "BNBUSDT", "close": "485.70000000" }, - "accountInfo": { - "makerCommission": 0, - "takerCommission": 0, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": true, - "canWithdraw": false, - "canDeposit": false, - "updateTime": 1630151787045, - "accountType": "SPOT", - "balances": [ - { - "asset": "BNB", - "free": "0.01000000", - "locked": "0.04000000", - "total": 0.05, - "estimatedValue": 24.285, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "BTC", - "free": "0.00000100", - "locked": "0.00000000", - "total": 0.000001, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "BUSD", - "free": "9904.79690824", - "locked": "41.96541778" - }, - { - "asset": "ETH", - "free": "0.10001000", - "locked": "99.90687000", - "total": 100.00688, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "LTC", - "free": "499.75398000", - "locked": "0.00000000", - "total": 499.75398, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "TRX", - "free": "500000.00000000", - "locked": "0.00000000" - }, - { - "asset": "USDT", - "free": "379068.42127337", - "locked": "0.00000000" - }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } - ], - "permissions": ["SPOT"] - }, "symbolConfiguration": { "_id": "61167428434d24b0b57ce954", "key": "configuration", "enabled": true, "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "BNBUSDT", "LTCBUSD", "BTCBUSD", "ETHBUSD"], "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": true, "triggerAfter": 1 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 }, - "candles": { "interval": "1m", "limit": 10 }, "buy": { "enabled": true, "lastBuyPriceRemoveThreshold": 10, "athRestriction": { "enabled": false, - "candles": { "interval": "1d", "limit": 30 }, + "candles": { + "interval": "1d", + "limit": 30 + }, "restrictionPercentage": 0.9 }, "maxPurchaseAmount": -1, @@ -491,80 +447,22 @@ "symbol": "BTCUSDT", "close": "48942.03000000" }, - "accountInfo": { - "makerCommission": 0, - "takerCommission": 0, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": true, - "canWithdraw": false, - "canDeposit": false, - "updateTime": 1629594092089, - "accountType": "SPOT", - "balances": [ - { - "asset": "BNB", - "free": "0.01000000", - "locked": "0.04000000", - "total": 0.05, - "estimatedValue": 0, - "updatedAt": "2021-08-22T01:01:32+00:00" - }, - { - "asset": "BTC", - "free": "0.00000100", - "locked": "0.00000000", - "total": 0.000001, - "estimatedValue": 0.04894203, - "updatedAt": "2021-08-22T01:01:32+00:00", - "isLessThanMinNotionalValue": true - }, - { - "asset": "BUSD", - "free": "9872.94875823", - "locked": "40.96271040" - }, - { - "asset": "ETH", - "free": "0.10001000", - "locked": "99.90687000", - "total": 100.00688, - "estimatedValue": 0, - "updatedAt": "2021-08-22T01:01:32+00:00" - }, - { - "asset": "LTC", - "free": "499.75398000", - "locked": "0.00000000", - "total": 499.75398, - "estimatedValue": 35182.68019200001, - "updatedAt": "2021-08-22T01:01:32+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "TRX", - "free": "500000.00000000", - "locked": "0.00000000" - }, - { - "asset": "USDT", - "free": "378987.63247205", - "locked": "20.97022808" - }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } - ], - "permissions": ["SPOT"] - }, "symbolConfiguration": { "_id": "611e5f2dd3630e44f849b05b", "key": "BTCUSDT-configuration", - "candles": { "interval": "1m", "limit": 10 }, + "candles": { + "interval": "1m", + "limit": 10 + }, "buy": { "enabled": true, "lastBuyPriceRemoveThreshold": 10, "athRestriction": { "enabled": false, - "candles": { "interval": "1d", "limit": 30 }, + "candles": { + "interval": "1d", + "limit": 30 + }, "restrictionPercentage": 0.9 }, "maxPurchaseAmount": -1, @@ -617,12 +515,17 @@ } }, "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": true, "triggerAfter": 1 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } }, "enabled": true, "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "BNBUSDT", "LTCBUSD", "BTCBUSD", "ETHBUSD"], "system": { "temporaryDisableActionAfterConfirmingOrder": 20, "checkManualBuyOrderPeriod": 5, @@ -758,7 +661,29 @@ }, "order": {}, "saveToCache": true, - "isActionDisabled": { "isDisabled": false, "ttl": -2 } + "tradingView": { + "request": { + "symbol": "BTCUSDT", + "screener": "CRYPTO", + "exchange": "BINANCE", + "interval": "1h" + }, + "result": { + "indicators": { + "summary": { + "BUY": 14, + "NEUTRAL": 10, + "RECOMMENDATION": "BUY", + "SELL": 2 + }, + "time": "2022-07-18T11:45:39.308508" + } + } + }, + "isActionDisabled": { + "isDisabled": false, + "ttl": -2 + } }, { "symbol": "ETHBUSD", @@ -773,89 +698,34 @@ "symbol": "ETHBUSD", "close": "1000.01000000" }, - "accountInfo": { - "makerCommission": 0, - "takerCommission": 0, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": true, - "canWithdraw": false, - "canDeposit": false, - "updateTime": 1630151787045, - "accountType": "SPOT", - "balances": [ - { - "asset": "BNB", - "free": "0.01000000", - "locked": "0.04000000", - "total": 0.05, - "estimatedValue": 24.285, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "BTC", - "free": "0.00000100", - "locked": "0.00000000", - "total": 0.000001, - "estimatedValue": 0.04887893, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": true - }, - { - "asset": "BUSD", - "free": "9904.79690824", - "locked": "41.96541778" - }, - { - "asset": "ETH", - "free": "0.10001000", - "locked": "99.90687000", - "total": 100.00688, - "estimatedValue": 100007.88006879999, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "LTC", - "free": "499.75398000", - "locked": "0.00000000", - "total": 499.75398, - "estimatedValue": 35192.675271600005, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "TRX", - "free": "500000.00000000", - "locked": "0.00000000" - }, - { - "asset": "USDT", - "free": "379068.42127337", - "locked": "0.00000000" - }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } - ], - "permissions": ["SPOT"] - }, "symbolConfiguration": { "_id": "61167428434d24b0b57ce954", "key": "configuration", "enabled": true, "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "BNBUSDT", "LTCBUSD", "BTCBUSD", "ETHBUSD"], "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": true, "triggerAfter": 1 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 }, - "candles": { "interval": "1m", "limit": 10 }, "buy": { "enabled": true, "lastBuyPriceRemoveThreshold": 10, "athRestriction": { "enabled": false, - "candles": { "interval": "1d", "limit": 30 }, + "candles": { + "interval": "1d", + "limit": 30 + }, "restrictionPercentage": 0.9 }, "maxPurchaseAmount": -1, @@ -1060,7 +930,10 @@ }, "order": {}, "saveToCache": true, - "isActionDisabled": { "isDisabled": false, "ttl": -2 } + "isActionDisabled": { + "isDisabled": false, + "ttl": -2 + } }, { "symbol": "BTCBUSD", @@ -1075,88 +948,34 @@ "symbol": "BTCBUSD", "close": "48878.93000000" }, - "accountInfo": { - "makerCommission": 0, - "takerCommission": 0, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": true, - "canWithdraw": false, - "canDeposit": false, - "updateTime": 1630151787045, - "accountType": "SPOT", - "balances": [ - { - "asset": "BNB", - "free": "0.01000000", - "locked": "0.04000000", - "total": 0.05, - "estimatedValue": 24.285, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "BTC", - "free": "0.00000100", - "locked": "0.00000000", - "total": 0.000001, - "estimatedValue": 0.04887893, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": true - }, - { - "asset": "BUSD", - "free": "9904.79690824", - "locked": "41.96541778" - }, - { - "asset": "ETH", - "free": "0.10001000", - "locked": "99.90687000", - "total": 100.00688, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "LTC", - "free": "499.75398000", - "locked": "0.00000000", - "total": 499.75398, - "estimatedValue": 35192.675271600005, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "TRX", - "free": "500000.00000000", - "locked": "0.00000000" - }, - { - "asset": "USDT", - "free": "379068.42127337", - "locked": "0.00000000" - }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } - ], - "permissions": ["SPOT"] - }, "symbolConfiguration": { "_id": "61167428434d24b0b57ce954", "key": "configuration", "enabled": true, "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "BNBUSDT", "LTCBUSD", "BTCBUSD", "ETHBUSD"], "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": true, "triggerAfter": 1 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 }, - "candles": { "interval": "1m", "limit": 10 }, "buy": { "enabled": true, "lastBuyPriceRemoveThreshold": 10, "athRestriction": { "enabled": false, - "candles": { "interval": "1d", "limit": 30 }, + "candles": { + "interval": "1d", + "limit": 30 + }, "restrictionPercentage": 0.9 }, "maxPurchaseAmount": -1, @@ -1343,7 +1162,10 @@ }, "order": {}, "saveToCache": true, - "isActionDisabled": { "isDisabled": false, "ttl": -2 } + "isActionDisabled": { + "isDisabled": false, + "ttl": -2 + } }, { "symbol": "LTCBUSD", @@ -1358,87 +1180,34 @@ "symbol": "LTCBUSD", "close": "70.42000000" }, - "accountInfo": { - "makerCommission": 0, - "takerCommission": 0, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": true, - "canWithdraw": false, - "canDeposit": false, - "updateTime": 1630151787045, - "accountType": "SPOT", - "balances": [ - { - "asset": "BNB", - "free": "0.01000000", - "locked": "0.04000000", - "total": 0.05, - "estimatedValue": 24.285, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "BTC", - "free": "0.00000100", - "locked": "0.00000000", - "total": 0.000001, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "BUSD", - "free": "9904.79690824", - "locked": "41.96541778" - }, - { - "asset": "ETH", - "free": "0.10001000", - "locked": "99.90687000", - "total": 100.00688, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "LTC", - "free": "499.75398000", - "locked": "0.00000000", - "total": 499.75398, - "estimatedValue": 35192.675271600005, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "TRX", - "free": "500000.00000000", - "locked": "0.00000000" - }, - { - "asset": "USDT", - "free": "379068.42127337", - "locked": "0.00000000" - }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } - ], - "permissions": ["SPOT"] - }, "symbolConfiguration": { "_id": "61167428434d24b0b57ce954", "key": "configuration", "enabled": true, "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "BNBUSDT", "LTCBUSD", "BTCBUSD", "ETHBUSD"], "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": true, "triggerAfter": 1 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 }, - "candles": { "interval": "1m", "limit": 10 }, "buy": { "enabled": true, "lastBuyPriceRemoveThreshold": 10, "athRestriction": { "enabled": false, - "candles": { "interval": "1d", "limit": 30 }, + "candles": { + "interval": "1d", + "limit": 30 + }, "restrictionPercentage": 0.9 }, "maxPurchaseAmount": -1, @@ -1625,7 +1394,10 @@ }, "order": {}, "saveToCache": true, - "isActionDisabled": { "isDisabled": false, "ttl": -2 } + "isActionDisabled": { + "isDisabled": false, + "ttl": -2 + } } ] } diff --git a/app/frontend/websocket/handlers/__tests__/fixtures/latest-stats-not-authenticated-unlock-list.json b/app/frontend/websocket/handlers/__tests__/fixtures/latest-stats-not-authenticated-unlock-list.json index 4fcac2d2..7f9ec51f 100644 --- a/app/frontend/websocket/handlers/__tests__/fixtures/latest-stats-not-authenticated-unlock-list.json +++ b/app/frontend/websocket/handlers/__tests__/fixtures/latest-stats-not-authenticated-unlock-list.json @@ -3,17 +3,42 @@ "type": "latest", "isAuthenticated": false, "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": false, "triggerAfter": 20 }, - "orderLimit": { "enabled": true, "maxBuyOpenOrders": 3, "maxOpenTrades": 5 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": false, + "triggerAfter": 20 + }, + "orderLimit": { + "enabled": true, + "maxBuyOpenOrders": 3, + "maxOpenTrades": 5 + } }, "configuration": { "enabled": true, "type": "i-am-global", - "candles": { "interval": "15m" }, + "symbols": [ + "BTCUSDT", + "BNBUSDT", + "ETHBUSD", + "BTCBUSD", + "LTCBUSD" + ], + "candles": { + "interval": "15m" + }, "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": false, "triggerAfter": 20 }, + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": false, + "triggerAfter": 20 + }, "orderLimit": { "enabled": true, "maxBuyOpenOrders": 3, @@ -23,7 +48,7 @@ "sell": {} }, "common": { - "version": "0.0.78", + "version": "0.0.85", "gitHash": "some-hash", "accountInfo": { "makerCommission": 0, @@ -36,109 +61,89 @@ "updateTime": 1630151787045, "accountType": "SPOT", "balances": [ - { "asset": "BNB", "free": "0.01000000", "locked": "0.04000000" }, - { "asset": "BTC", "free": "0.00000100", "locked": "0.00000000" }, - { "asset": "BUSD", "free": "9904.79690824", "locked": "41.96541778" }, - { "asset": "ETH", "free": "0.10001000", "locked": "99.90687000" }, - { "asset": "LTC", "free": "499.75398000", "locked": "0.00000000" }, - { "asset": "TRX", "free": "500000.00000000", "locked": "0.00000000" }, - { "asset": "USDT", "free": "379068.42127337", "locked": "0.00000000" }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } + { + "asset": "BNB", + "free": "0.01000000", + "locked": "0.04000000", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "BTC", + "free": "0.00000100", + "locked": "0.00000000", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "BUSD", + "free": "9904.79690824", + "locked": "41.96541778", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "ETH", + "free": "0.10001000", + "locked": "99.90687000", + "quote": "USDT", + "estimate": "1574.50", + "tickSize": "0.01000000" + }, + { + "asset": "LTC", + "free": "499.75398000", + "locked": "0.00000000", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "TRX", + "free": "500000.00000000", + "locked": "0.00000000", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "USDT", + "free": "379068.42127337", + "locked": "0.00000000", + "quote": null, + "estimate": null, + "tickSize": null + }, + { + "asset": "XRP", + "free": "50000.00000000", + "locked": "0.00000000", + "quote": null, + "estimate": null, + "tickSize": null + } ], - "permissions": ["SPOT"] + "permissions": [ + "SPOT" + ] }, - "exchangeSymbols": { - "BNBBUSD": { - "symbol": "BNBBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 - }, - "BTCBUSD": { - "symbol": "BTCBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 - }, - "ETHBUSD": { - "symbol": "ETHBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 - }, - "LTCBUSD": { - "symbol": "LTCBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 - }, - "TRXBUSD": { - "symbol": "TRXBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 + "apiInfo": { + "spot": { + "usedWeight1m": "60" }, - "XRPBUSD": { - "symbol": "XRPBUSD", - "quoteAsset": "BUSD", - "minNotional": 10 - }, - "BNBUSDT": { - "symbol": "BNBUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "BTCUSDT": { - "symbol": "BTCUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "ETHUSDT": { - "symbol": "ETHUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "LTCUSDT": { - "symbol": "LTCUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "TRXUSDT": { - "symbol": "TRXUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "XRPUSDT": { - "symbol": "XRPUSDT", - "quoteAsset": "USDT", - "minNotional": 10 - }, - "BNBBTC": { - "symbol": "BNBBTC", - "quoteAsset": "BTC", - "minNotional": 0.0001 - }, - "ETHBTC": { - "symbol": "ETHBTC", - "quoteAsset": "BTC", - "minNotional": 0.0001 - }, - "LTCBTC": { - "symbol": "LTCBTC", - "quoteAsset": "BTC", - "minNotional": 0.0001 - }, - "TRXBTC": { - "symbol": "TRXBTC", - "quoteAsset": "BTC", - "minNotional": 0.0001 - }, - "XRPBTC": { - "symbol": "XRPBTC", - "quoteAsset": "BTC", - "minNotional": 0.0001 - }, - "LTCBNB": { "symbol": "LTCBNB", "quoteAsset": "BNB", "minNotional": 0.1 }, - "TRXBNB": { "symbol": "TRXBNB", "quoteAsset": "BNB", "minNotional": 0.1 }, - "XRPBNB": { "symbol": "XRPBNB", "quoteAsset": "BNB", "minNotional": 0.1 } + "futures": {} + }, + "closedTradesSetting": { + "loadedPeriod": "a", + "selectedPeriod": "a" + }, + "orderStats": { + "numberOfOpenTrades": 6, + "numberOfBuyOpenOrders": 3 }, - "apiInfo": { "spot": { "usedWeight1m": "60" }, "futures": {} }, - "closedTradesSetting": { "loadedPeriod": "a", "selectedPeriod": "a" }, "closedTrades": [ { "_id": "USDT", @@ -169,7 +174,10 @@ "profitPercentage": 1601.995003799773 } ], - "orderStats": { "numberOfOpenTrades": 6, "numberOfBuyOpenOrders": 3 } + "totalProfitAndLoss": [], + "streamsCount": 6, + "symbolsCount": 5, + "totalPages": 1 }, "stats": { "symbols": [ @@ -186,86 +194,34 @@ "symbol": "BNBUSDT", "close": "485.70000000" }, - "accountInfo": { - "makerCommission": 0, - "takerCommission": 0, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": true, - "canWithdraw": false, - "canDeposit": false, - "updateTime": 1630151787045, - "accountType": "SPOT", - "balances": [ - { - "asset": "BNB", - "free": "0.01000000", - "locked": "0.04000000", - "total": 0.05, - "estimatedValue": 24.285, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "BTC", - "free": "0.00000100", - "locked": "0.00000000", - "total": 0.000001, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "BUSD", - "free": "9904.79690824", - "locked": "41.96541778" - }, - { - "asset": "ETH", - "free": "0.10001000", - "locked": "99.90687000", - "total": 100.00688, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "LTC", - "free": "499.75398000", - "locked": "0.00000000", - "total": 499.75398, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "TRX", - "free": "500000.00000000", - "locked": "0.00000000" - }, - { - "asset": "USDT", - "free": "379068.42127337", - "locked": "0.00000000" - }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } - ], - "permissions": ["SPOT"] - }, "symbolConfiguration": { "_id": "61167428434d24b0b57ce954", "key": "configuration", "enabled": true, "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "BNBUSDT", "LTCBUSD", "BTCBUSD", "ETHBUSD"], "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": true, "triggerAfter": 1 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 }, - "candles": { "interval": "1m", "limit": 10 }, "buy": { "enabled": true, "lastBuyPriceRemoveThreshold": 10, "athRestriction": { "enabled": false, - "candles": { "interval": "1d", "limit": 30 }, + "candles": { + "interval": "1d", + "limit": 30 + }, "restrictionPercentage": 0.9 }, "maxPurchaseAmount": -1, @@ -491,80 +447,22 @@ "symbol": "BTCUSDT", "close": "48942.03000000" }, - "accountInfo": { - "makerCommission": 0, - "takerCommission": 0, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": true, - "canWithdraw": false, - "canDeposit": false, - "updateTime": 1629594092089, - "accountType": "SPOT", - "balances": [ - { - "asset": "BNB", - "free": "0.01000000", - "locked": "0.04000000", - "total": 0.05, - "estimatedValue": 0, - "updatedAt": "2021-08-22T01:01:32+00:00" - }, - { - "asset": "BTC", - "free": "0.00000100", - "locked": "0.00000000", - "total": 0.000001, - "estimatedValue": 0.04894203, - "updatedAt": "2021-08-22T01:01:32+00:00", - "isLessThanMinNotionalValue": true - }, - { - "asset": "BUSD", - "free": "9872.94875823", - "locked": "40.96271040" - }, - { - "asset": "ETH", - "free": "0.10001000", - "locked": "99.90687000", - "total": 100.00688, - "estimatedValue": 0, - "updatedAt": "2021-08-22T01:01:32+00:00" - }, - { - "asset": "LTC", - "free": "499.75398000", - "locked": "0.00000000", - "total": 499.75398, - "estimatedValue": 35182.68019200001, - "updatedAt": "2021-08-22T01:01:32+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "TRX", - "free": "500000.00000000", - "locked": "0.00000000" - }, - { - "asset": "USDT", - "free": "378987.63247205", - "locked": "20.97022808" - }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } - ], - "permissions": ["SPOT"] - }, "symbolConfiguration": { "_id": "611e5f2dd3630e44f849b05b", "key": "BTCUSDT-configuration", - "candles": { "interval": "1m", "limit": 10 }, + "candles": { + "interval": "1m", + "limit": 10 + }, "buy": { "enabled": true, "lastBuyPriceRemoveThreshold": 10, "athRestriction": { "enabled": false, - "candles": { "interval": "1d", "limit": 30 }, + "candles": { + "interval": "1d", + "limit": 30 + }, "restrictionPercentage": 0.9 }, "maxPurchaseAmount": -1, @@ -617,12 +515,17 @@ } }, "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": true, "triggerAfter": 1 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } }, "enabled": true, "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "BNBUSDT", "LTCBUSD", "BTCBUSD", "ETHBUSD"], "system": { "temporaryDisableActionAfterConfirmingOrder": 20, "checkManualBuyOrderPeriod": 5, @@ -758,7 +661,29 @@ }, "order": {}, "saveToCache": true, - "isActionDisabled": { "isDisabled": false, "ttl": -2 } + "tradingView": { + "request": { + "symbol": "BTCUSDT", + "screener": "CRYPTO", + "exchange": "BINANCE", + "interval": "1h" + }, + "result": { + "indicators": { + "summary": { + "BUY": 14, + "NEUTRAL": 10, + "RECOMMENDATION": "BUY", + "SELL": 2 + }, + "time": "2022-07-18T11:45:39.308508" + } + } + }, + "isActionDisabled": { + "isDisabled": false, + "ttl": -2 + } }, { "symbol": "ETHBUSD", @@ -773,89 +698,34 @@ "symbol": "ETHBUSD", "close": "1000.01000000" }, - "accountInfo": { - "makerCommission": 0, - "takerCommission": 0, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": true, - "canWithdraw": false, - "canDeposit": false, - "updateTime": 1630151787045, - "accountType": "SPOT", - "balances": [ - { - "asset": "BNB", - "free": "0.01000000", - "locked": "0.04000000", - "total": 0.05, - "estimatedValue": 24.285, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "BTC", - "free": "0.00000100", - "locked": "0.00000000", - "total": 0.000001, - "estimatedValue": 0.04887893, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": true - }, - { - "asset": "BUSD", - "free": "9904.79690824", - "locked": "41.96541778" - }, - { - "asset": "ETH", - "free": "0.10001000", - "locked": "99.90687000", - "total": 100.00688, - "estimatedValue": 100007.88006879999, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "LTC", - "free": "499.75398000", - "locked": "0.00000000", - "total": 499.75398, - "estimatedValue": 35192.675271600005, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "TRX", - "free": "500000.00000000", - "locked": "0.00000000" - }, - { - "asset": "USDT", - "free": "379068.42127337", - "locked": "0.00000000" - }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } - ], - "permissions": ["SPOT"] - }, "symbolConfiguration": { "_id": "61167428434d24b0b57ce954", "key": "configuration", "enabled": true, "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "BNBUSDT", "LTCBUSD", "BTCBUSD", "ETHBUSD"], "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": true, "triggerAfter": 1 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 }, - "candles": { "interval": "1m", "limit": 10 }, "buy": { "enabled": true, "lastBuyPriceRemoveThreshold": 10, "athRestriction": { "enabled": false, - "candles": { "interval": "1d", "limit": 30 }, + "candles": { + "interval": "1d", + "limit": 30 + }, "restrictionPercentage": 0.9 }, "maxPurchaseAmount": -1, @@ -1060,7 +930,10 @@ }, "order": {}, "saveToCache": true, - "isActionDisabled": { "isDisabled": false, "ttl": -2 } + "isActionDisabled": { + "isDisabled": false, + "ttl": -2 + } }, { "symbol": "BTCBUSD", @@ -1075,88 +948,34 @@ "symbol": "BTCBUSD", "close": "48878.93000000" }, - "accountInfo": { - "makerCommission": 0, - "takerCommission": 0, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": true, - "canWithdraw": false, - "canDeposit": false, - "updateTime": 1630151787045, - "accountType": "SPOT", - "balances": [ - { - "asset": "BNB", - "free": "0.01000000", - "locked": "0.04000000", - "total": 0.05, - "estimatedValue": 24.285, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "BTC", - "free": "0.00000100", - "locked": "0.00000000", - "total": 0.000001, - "estimatedValue": 0.04887893, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": true - }, - { - "asset": "BUSD", - "free": "9904.79690824", - "locked": "41.96541778" - }, - { - "asset": "ETH", - "free": "0.10001000", - "locked": "99.90687000", - "total": 100.00688, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "LTC", - "free": "499.75398000", - "locked": "0.00000000", - "total": 499.75398, - "estimatedValue": 35192.675271600005, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "TRX", - "free": "500000.00000000", - "locked": "0.00000000" - }, - { - "asset": "USDT", - "free": "379068.42127337", - "locked": "0.00000000" - }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } - ], - "permissions": ["SPOT"] - }, "symbolConfiguration": { "_id": "61167428434d24b0b57ce954", "key": "configuration", "enabled": true, "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "BNBUSDT", "LTCBUSD", "BTCBUSD", "ETHBUSD"], "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": true, "triggerAfter": 1 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 }, - "candles": { "interval": "1m", "limit": 10 }, "buy": { "enabled": true, "lastBuyPriceRemoveThreshold": 10, "athRestriction": { "enabled": false, - "candles": { "interval": "1d", "limit": 30 }, + "candles": { + "interval": "1d", + "limit": 30 + }, "restrictionPercentage": 0.9 }, "maxPurchaseAmount": -1, @@ -1343,7 +1162,10 @@ }, "order": {}, "saveToCache": true, - "isActionDisabled": { "isDisabled": false, "ttl": -2 } + "isActionDisabled": { + "isDisabled": false, + "ttl": -2 + } }, { "symbol": "LTCBUSD", @@ -1358,87 +1180,34 @@ "symbol": "LTCBUSD", "close": "70.42000000" }, - "accountInfo": { - "makerCommission": 0, - "takerCommission": 0, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": true, - "canWithdraw": false, - "canDeposit": false, - "updateTime": 1630151787045, - "accountType": "SPOT", - "balances": [ - { - "asset": "BNB", - "free": "0.01000000", - "locked": "0.04000000", - "total": 0.05, - "estimatedValue": 24.285, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "BTC", - "free": "0.00000100", - "locked": "0.00000000", - "total": 0.000001, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "BUSD", - "free": "9904.79690824", - "locked": "41.96541778" - }, - { - "asset": "ETH", - "free": "0.10001000", - "locked": "99.90687000", - "total": 100.00688, - "estimatedValue": 0, - "updatedAt": "2021-08-28T11:56:27+00:00" - }, - { - "asset": "LTC", - "free": "499.75398000", - "locked": "0.00000000", - "total": 499.75398, - "estimatedValue": 35192.675271600005, - "updatedAt": "2021-08-28T11:56:27+00:00", - "isLessThanMinNotionalValue": false - }, - { - "asset": "TRX", - "free": "500000.00000000", - "locked": "0.00000000" - }, - { - "asset": "USDT", - "free": "379068.42127337", - "locked": "0.00000000" - }, - { "asset": "XRP", "free": "50000.00000000", "locked": "0.00000000" } - ], - "permissions": ["SPOT"] - }, "symbolConfiguration": { "_id": "61167428434d24b0b57ce954", "key": "configuration", "enabled": true, "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "BNBUSDT", "LTCBUSD", "BTCBUSD", "ETHBUSD"], "botOptions": { - "authentication": { "lockList": false, "lockAfter": 120 }, - "autoTriggerBuy": { "enabled": true, "triggerAfter": 1 } + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 }, - "candles": { "interval": "1m", "limit": 10 }, "buy": { "enabled": true, "lastBuyPriceRemoveThreshold": 10, "athRestriction": { "enabled": false, - "candles": { "interval": "1d", "limit": 30 }, + "candles": { + "interval": "1d", + "limit": 30 + }, "restrictionPercentage": 0.9 }, "maxPurchaseAmount": -1, @@ -1625,7 +1394,10 @@ }, "order": {}, "saveToCache": true, - "isActionDisabled": { "isDisabled": false, "ttl": -2 } + "isActionDisabled": { + "isDisabled": false, + "ttl": -2 + } } ] } diff --git a/app/frontend/websocket/handlers/__tests__/fixtures/latest-trailing-trade-symbols.json b/app/frontend/websocket/handlers/__tests__/fixtures/latest-trailing-trade-symbols.json index 4fc285c5..e4e24c5d 100644 --- a/app/frontend/websocket/handlers/__tests__/fixtures/latest-trailing-trade-symbols.json +++ b/app/frontend/websocket/handlers/__tests__/fixtures/latest-trailing-trade-symbols.json @@ -1,8 +1,1178 @@ -{ - "BNBUSDT-processed-data": "{\"symbol\":\"BNBUSDT\",\"isLocked\":false,\"featureToggle\":{\"notifyOrderConfirm\":true,\"notifyDebug\":true,\"notifyOrderExecute\":true},\"lastCandle\":{\"eventType\":\"kline\",\"symbol\":\"BNBUSDT\",\"close\":\"485.70000000\"},\"accountInfo\":{\"makerCommission\":0,\"takerCommission\":0,\"buyerCommission\":0,\"sellerCommission\":0,\"canTrade\":true,\"canWithdraw\":false,\"canDeposit\":false,\"updateTime\":1630151787045,\"accountType\":\"SPOT\",\"balances\":[{\"asset\":\"BNB\",\"free\":\"0.01000000\",\"locked\":\"0.04000000\",\"total\":0.05,\"estimatedValue\":24.285,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},{\"asset\":\"BTC\",\"free\":\"0.00000100\",\"locked\":\"0.00000000\",\"total\":0.000001,\"estimatedValue\":0,\"updatedAt\":\"2021-08-28T11:56:27+00:00\"},{\"asset\":\"BUSD\",\"free\":\"9904.79690824\",\"locked\":\"41.96541778\"},{\"asset\":\"ETH\",\"free\":\"0.10001000\",\"locked\":\"99.90687000\",\"total\":100.00688,\"estimatedValue\":0,\"updatedAt\":\"2021-08-28T11:56:27+00:00\"},{\"asset\":\"LTC\",\"free\":\"499.75398000\",\"locked\":\"0.00000000\",\"total\":499.75398,\"estimatedValue\":0,\"updatedAt\":\"2021-08-28T11:56:27+00:00\"},{\"asset\":\"TRX\",\"free\":\"500000.00000000\",\"locked\":\"0.00000000\"},{\"asset\":\"USDT\",\"free\":\"379068.42127337\",\"locked\":\"0.00000000\"},{\"asset\":\"XRP\",\"free\":\"50000.00000000\",\"locked\":\"0.00000000\"}],\"permissions\":[\"SPOT\"]},\"symbolConfiguration\":{\"_id\":\"61167428434d24b0b57ce954\",\"key\":\"configuration\",\"enabled\":true,\"cronTime\":\"* * * * * *\",\"symbols\":[\"BTCUSDT\",\"BNBUSDT\",\"LTCBUSD\",\"BTCBUSD\",\"ETHBUSD\"],\"botOptions\":{\"authentication\":{\"lockList\":false,\"lockAfter\":120},\"autoTriggerBuy\":{\"enabled\":true,\"triggerAfter\":1}},\"candles\":{\"interval\":\"1m\",\"limit\":10},\"buy\":{\"enabled\":true,\"lastBuyPriceRemoveThreshold\":10,\"athRestriction\":{\"enabled\":false,\"candles\":{\"interval\":\"1d\",\"limit\":30},\"restrictionPercentage\":0.9},\"maxPurchaseAmount\":-1,\"gridTrade\":[{\"triggerPercentage\":1,\"stopPercentage\":1.001,\"limitPercentage\":1.0011,\"maxPurchaseAmount\":21,\"executed\":true,\"executedOrder\":{\"symbol\":\"BNBUSDT\",\"orderId\":5213213,\"orderListId\":-1,\"clientOrderId\":\"duQYBSnlLOoDgcN3Xa1OsO\",\"price\":\"449.52000000\",\"origQty\":\"0.04000000\",\"executedQty\":\"0.04000000\",\"cummulativeQuoteQty\":\"17.98080000\",\"status\":\"FILLED\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"BUY\",\"stopPrice\":\"449.47000000\",\"icebergQty\":\"0.00000000\",\"time\":1629590089149,\"updateTime\":1629590131092,\"isWorking\":true,\"origQuoteOrderQty\":\"0.00000000\",\"currentGridTradeIndex\":0,\"nextCheck\":\"2021-08-21T23:55:33.247Z\"}}],\"currentGridTradeIndex\":-1,\"currentGridTrade\":null},\"sell\":{\"enabled\":true,\"stopLoss\":{\"enabled\":true,\"maxLossPercentage\":0.99,\"disableBuyMinutes\":1,\"orderType\":\"market\"},\"gridTrade\":[{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}],\"currentGridTradeIndex\":0,\"currentGridTrade\":{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}},\"system\":{\"temporaryDisableActionAfterConfirmingOrder\":20,\"checkManualBuyOrderPeriod\":5,\"placeManualOrderInterval\":5,\"refreshAccountInfoPeriod\":1,\"checkOrderExecutePeriod\":10,\"checkManualOrderPeriod\":5}},\"indicators\":{\"highestPrice\":485.9,\"lowestPrice\":484.1,\"athPrice\":null},\"symbolInfo\":{\"symbol\":\"BNBUSDT\",\"status\":\"TRADING\",\"baseAsset\":\"BNB\",\"baseAssetPrecision\":8,\"quoteAsset\":\"USDT\",\"quotePrecision\":8,\"filterLotSize\":{\"filterType\":\"LOT_SIZE\",\"minQty\":\"0.01000000\",\"maxQty\":\"9000.00000000\",\"stepSize\":\"0.01000000\"},\"filterPrice\":{\"filterType\":\"PRICE_FILTER\",\"minPrice\":\"0.01000000\",\"maxPrice\":\"10000.00000000\",\"tickSize\":\"0.01000000\"},\"filterMinNotional\":{\"filterType\":\"MIN_NOTIONAL\",\"minNotional\":\"10.00000000\",\"applyToMarket\":true,\"avgPriceMins\":5}},\"openOrders\":[{\"symbol\":\"BNBUSDT\",\"orderId\":6398822,\"orderListId\":-1,\"clientOrderId\":\"m471GLEsN8YzBDVweWQgN0\",\"price\":\"643.11000000\",\"origQty\":\"0.04000000\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"SELL\",\"stopPrice\":\"643.75000000\",\"icebergQty\":\"0.00000000\",\"time\":1630129419133,\"updateTime\":1630129428568,\"isWorking\":true,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":485.7,\"updatedAt\":\"2021-08-28T05:43:39.133Z\",\"differenceToExecute\":-32.54066296067533,\"differenceToCancel\":-32.80627551169872,\"minimumProfit\":6.163200000000002,\"minimumProfitPercentage\":31.507269492669177}],\"action\":\"sell-order-wait\",\"baseAssetBalance\":{\"asset\":\"BNB\",\"free\":\"0.01000000\",\"locked\":\"0.04000000\",\"total\":0.05,\"estimatedValue\":24.285,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},\"quoteAssetBalance\":{\"asset\":\"USDT\",\"free\":\"379068.42127337\",\"locked\":\"0.00000000\"},\"buy\":{\"currentPrice\":485.7,\"limitPrice\":null,\"highestPrice\":485.9,\"lowestPrice\":484.1,\"athPrice\":null,\"athRestrictionPrice\":null,\"triggerPrice\":null,\"difference\":null,\"openOrders\":[],\"processMessage\":\"\",\"updatedAt\":\"2021-08-28T12:09:50.048Z\"},\"sell\":{\"currentPrice\":485.7,\"limitPrice\":484.7286,\"lastBuyPrice\":489.03,\"triggerPrice\":489.51902999999993,\"difference\":-0.7862940086472925,\"stopLossTriggerPrice\":484.13969999999995,\"stopLossDifference\":0.32124768375541013,\"currentProfit\":-0.1664999999999992,\"currentProfitPercentage\":-0.6809398196429672,\"openOrders\":[{\"symbol\":\"BNBUSDT\",\"orderId\":6398822,\"orderListId\":-1,\"clientOrderId\":\"m471GLEsN8YzBDVweWQgN0\",\"price\":\"643.11000000\",\"origQty\":\"0.04000000\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"SELL\",\"stopPrice\":\"643.75000000\",\"icebergQty\":\"0.00000000\",\"time\":1630129419133,\"updateTime\":1630129428568,\"isWorking\":true,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":485.7,\"updatedAt\":\"2021-08-28T05:43:39.133Z\",\"differenceToExecute\":-32.54066296067533,\"differenceToCancel\":-32.80627551169872,\"minimumProfit\":6.163200000000002,\"minimumProfitPercentage\":31.507269492669177}],\"processMessage\":\"\",\"updatedAt\":\"2021-08-28T12:09:50.048Z\"},\"order\":{},\"saveToCache\":true}", - "BTCUSDT-processed-data": "{\"symbol\":\"BTCUSDT\",\"isLocked\":false,\"featureToggle\":{\"notifyOrderConfirm\":true,\"notifyDebug\":true,\"notifyOrderExecute\":true},\"lastCandle\":{\"eventType\":\"kline\",\"symbol\":\"BTCUSDT\",\"close\":\"48942.03000000\"},\"accountInfo\":{\"makerCommission\":0,\"takerCommission\":0,\"buyerCommission\":0,\"sellerCommission\":0,\"canTrade\":true,\"canWithdraw\":false,\"canDeposit\":false,\"updateTime\":1629594092089,\"accountType\":\"SPOT\",\"balances\":[{\"asset\":\"BNB\",\"free\":\"0.01000000\",\"locked\":\"0.04000000\",\"total\":0.05,\"estimatedValue\":0,\"updatedAt\":\"2021-08-22T01:01:32+00:00\"},{\"asset\":\"BTC\",\"free\":\"0.00000100\",\"locked\":\"0.00000000\",\"total\":0.000001,\"estimatedValue\":0.04894203,\"updatedAt\":\"2021-08-22T01:01:32+00:00\",\"isLessThanMinNotionalValue\":true},{\"asset\":\"BUSD\",\"free\":\"9872.94875823\",\"locked\":\"40.96271040\"},{\"asset\":\"ETH\",\"free\":\"0.10001000\",\"locked\":\"99.90687000\",\"total\":100.00688,\"estimatedValue\":0,\"updatedAt\":\"2021-08-22T01:01:32+00:00\"},{\"asset\":\"LTC\",\"free\":\"499.75398000\",\"locked\":\"0.00000000\",\"total\":499.75398,\"estimatedValue\":35182.68019200001,\"updatedAt\":\"2021-08-22T01:01:32+00:00\",\"isLessThanMinNotionalValue\":false},{\"asset\":\"TRX\",\"free\":\"500000.00000000\",\"locked\":\"0.00000000\"},{\"asset\":\"USDT\",\"free\":\"378987.63247205\",\"locked\":\"20.97022808\"},{\"asset\":\"XRP\",\"free\":\"50000.00000000\",\"locked\":\"0.00000000\"}],\"permissions\":[\"SPOT\"]},\"symbolConfiguration\":{\"_id\":\"611e5f2dd3630e44f849b05b\",\"key\":\"BTCUSDT-configuration\",\"candles\":{\"interval\":\"1m\",\"limit\":10},\"buy\":{\"enabled\":true,\"lastBuyPriceRemoveThreshold\":10,\"athRestriction\":{\"enabled\":false,\"candles\":{\"interval\":\"1d\",\"limit\":30},\"restrictionPercentage\":0.9},\"maxPurchaseAmount\":-1,\"gridTrade\":[{\"triggerPercentage\":1,\"stopPercentage\":1.001,\"limitPercentage\":1.0011,\"maxPurchaseAmount\":21,\"executed\":false,\"executedOrder\":null}],\"currentGridTradeIndex\":0,\"currentGridTrade\":{\"triggerPercentage\":1,\"stopPercentage\":1.001,\"limitPercentage\":1.0011,\"maxPurchaseAmount\":21,\"executed\":false,\"executedOrder\":null}},\"sell\":{\"enabled\":true,\"stopLoss\":{\"enabled\":true,\"maxLossPercentage\":0.99,\"disableBuyMinutes\":1,\"orderType\":\"market\"},\"gridTrade\":[{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}],\"currentGridTradeIndex\":0,\"currentGridTrade\":{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}},\"botOptions\":{\"authentication\":{\"lockList\":false,\"lockAfter\":120},\"autoTriggerBuy\":{\"enabled\":true,\"triggerAfter\":1}},\"enabled\":true,\"cronTime\":\"* * * * * *\",\"symbols\":[\"BTCUSDT\",\"BNBUSDT\",\"LTCBUSD\",\"BTCBUSD\",\"ETHBUSD\"],\"system\":{\"temporaryDisableActionAfterConfirmingOrder\":20,\"checkManualBuyOrderPeriod\":5,\"placeManualOrderInterval\":5,\"refreshAccountInfoPeriod\":1,\"checkOrderExecutePeriod\":10,\"checkManualOrderPeriod\":5}},\"indicators\":{\"highestPrice\":49015.11,\"lowestPrice\":48892.71,\"athPrice\":null},\"symbolInfo\":{\"symbol\":\"BTCUSDT\",\"status\":\"TRADING\",\"baseAsset\":\"BTC\",\"baseAssetPrecision\":8,\"quoteAsset\":\"USDT\",\"quotePrecision\":8,\"filterLotSize\":{\"filterType\":\"LOT_SIZE\",\"minQty\":\"0.00000100\",\"maxQty\":\"900.00000000\",\"stepSize\":\"0.00000100\"},\"filterPrice\":{\"filterType\":\"PRICE_FILTER\",\"minPrice\":\"0.01000000\",\"maxPrice\":\"1000000.00000000\",\"tickSize\":\"0.01000000\"},\"filterMinNotional\":{\"filterType\":\"MIN_NOTIONAL\",\"minNotional\":\"10.00000000\",\"applyToMarket\":true,\"avgPriceMins\":5}},\"openOrders\":[{\"symbol\":\"BTCUSDT\",\"orderId\":7010265,\"orderListId\":-1,\"clientOrderId\":\"Gox2Mu7nLcDBeAj6mZFVVU\",\"price\":\"48995.86000000\",\"origQty\":\"0.00042800\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"BUY\",\"stopPrice\":\"48990.97000000\",\"icebergQty\":\"0.00000000\",\"time\":1629594092089,\"updateTime\":1629594092089,\"isWorking\":false,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":48942.03,\"updatedAt\":\"2021-08-22T01:01:32.089Z\",\"differenceToExecute\":-0.09999585223581242,\"differenceToCancel\":0.009993155293375189}],\"action\":\"buy-order-wait\",\"baseAssetBalance\":{\"asset\":\"BTC\",\"free\":\"0.00000100\",\"locked\":\"0.00000000\",\"total\":0.000001,\"estimatedValue\":0.04894203,\"updatedAt\":\"2021-08-22T01:01:32+00:00\",\"isLessThanMinNotionalValue\":true},\"quoteAssetBalance\":{\"asset\":\"USDT\",\"free\":\"378987.63247205\",\"locked\":\"20.97022808\"},\"buy\":{\"currentPrice\":48942.03,\"limitPrice\":48995.866233,\"highestPrice\":49015.11,\"lowestPrice\":48892.71,\"athPrice\":null,\"athRestrictionPrice\":null,\"triggerPrice\":48892.71,\"difference\":0.10087393396684963,\"openOrders\":[{\"symbol\":\"BTCUSDT\",\"orderId\":7010265,\"orderListId\":-1,\"clientOrderId\":\"Gox2Mu7nLcDBeAj6mZFVVU\",\"price\":\"48995.86000000\",\"origQty\":\"0.00042800\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"BUY\",\"stopPrice\":\"48990.97000000\",\"icebergQty\":\"0.00000000\",\"time\":1629594092089,\"updateTime\":1629594092089,\"isWorking\":false,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":48942.03,\"updatedAt\":\"2021-08-22T01:01:32.089Z\",\"differenceToExecute\":-0.09999585223581242,\"differenceToCancel\":0.009993155293375189}],\"processMessage\":\"\",\"updatedAt\":\"2021-08-22T01:04:08.210Z\"},\"sell\":{\"currentPrice\":48942.03,\"limitPrice\":null,\"lastBuyPrice\":null,\"triggerPrice\":null,\"difference\":null,\"stopLossTriggerPrice\":null,\"stopLossDifference\":null,\"currentProfit\":null,\"currentProfitPercentage\":null,\"openOrders\":[],\"processMessage\":\"\",\"updatedAt\":\"2021-08-22T01:04:08.210Z\"},\"order\":{},\"saveToCache\":true}", - "BTCUSDT-procssed-data": "{\"symbol\":\"BTCUSDT\",\"isLocked\":false,\"featureToggle\":{\"notifyOrderConfirm\":true,\"notifyDebug\":true,\"notifyOrderExecute\":true},\"lastCandle\":{\"eventType\":\"kline\",\"symbol\":\"BTCUSDT\",\"close\":\"48942.03000000\"},\"accountInfo\":{\"makerCommission\":0,\"takerCommission\":0,\"buyerCommission\":0,\"sellerCommission\":0,\"canTrade\":true,\"canWithdraw\":false,\"canDeposit\":false,\"updateTime\":1629594092089,\"accountType\":\"SPOT\",\"balances\":[{\"asset\":\"BNB\",\"free\":\"0.01000000\",\"locked\":\"0.04000000\",\"total\":0.05,\"estimatedValue\":0,\"updatedAt\":\"2021-08-22T01:01:32+00:00\"},{\"asset\":\"BTC\",\"free\":\"0.00000100\",\"locked\":\"0.00000000\",\"total\":0.000001,\"estimatedValue\":0.04894203,\"updatedAt\":\"2021-08-22T01:01:32+00:00\",\"isLessThanMinNotionalValue\":true},{\"asset\":\"BUSD\",\"free\":\"9872.94875823\",\"locked\":\"40.96271040\"},{\"asset\":\"ETH\",\"free\":\"0.10001000\",\"locked\":\"99.90687000\",\"total\":100.00688,\"estimatedValue\":0,\"updatedAt\":\"2021-08-22T01:01:32+00:00\"},{\"asset\":\"LTC\",\"free\":\"499.75398000\",\"locked\":\"0.00000000\",\"total\":499.75398,\"estimatedValue\":35182.68019200001,\"updatedAt\":\"2021-08-22T01:01:32+00:00\",\"isLessThanMinNotionalValue\":false},{\"asset\":\"TRX\",\"free\":\"500000.00000000\",\"locked\":\"0.00000000\"},{\"asset\":\"USDT\",\"free\":\"378987.63247205\",\"locked\":\"20.97022808\"},{\"asset\":\"XRP\",\"free\":\"50000.00000000\",\"locked\":\"0.00000000\"}],\"permissions\":[\"SPOT\"]},\"symbolConfiguration\":{\"_id\":\"611e5f2dd3630e44f849b05b\",\"key\":\"BTCUSDT-configuration\",\"candles\":{\"interval\":\"1m\",\"limit\":10},\"buy\":{\"enabled\":true,\"lastBuyPriceRemoveThreshold\":10,\"athRestriction\":{\"enabled\":false,\"candles\":{\"interval\":\"1d\",\"limit\":30},\"restrictionPercentage\":0.9},\"maxPurchaseAmount\":-1,\"gridTrade\":[{\"triggerPercentage\":1,\"stopPercentage\":1.001,\"limitPercentage\":1.0011,\"maxPurchaseAmount\":21,\"executed\":false,\"executedOrder\":null}],\"currentGridTradeIndex\":0,\"currentGridTrade\":{\"triggerPercentage\":1,\"stopPercentage\":1.001,\"limitPercentage\":1.0011,\"maxPurchaseAmount\":21,\"executed\":false,\"executedOrder\":null}},\"sell\":{\"enabled\":true,\"stopLoss\":{\"enabled\":true,\"maxLossPercentage\":0.99,\"disableBuyMinutes\":1,\"orderType\":\"market\"},\"gridTrade\":[{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}],\"currentGridTradeIndex\":0,\"currentGridTrade\":{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}},\"botOptions\":{\"authentication\":{\"lockList\":false,\"lockAfter\":120},\"autoTriggerBuy\":{\"enabled\":true,\"triggerAfter\":1}},\"enabled\":true,\"cronTime\":\"* * * * * *\",\"symbols\":[\"BTCUSDT\",\"BNBUSDT\",\"LTCBUSD\",\"BTCBUSD\",\"ETHBUSD\"],\"system\":{\"temporaryDisableActionAfterConfirmingOrder\":20,\"checkManualBuyOrderPeriod\":5,\"placeManualOrderInterval\":5,\"refreshAccountInfoPeriod\":1,\"checkOrderExecutePeriod\":10,\"checkManualOrderPeriod\":5}},\"indicators\":{\"highestPrice\":49015.11,\"lowestPrice\":48892.71,\"athPrice\":null},\"symbolInfo\":{\"symbol\":\"BTCUSDT\",\"status\":\"TRADING\",\"baseAsset\":\"BTC\",\"baseAssetPrecision\":8,\"quoteAsset\":\"USDT\",\"quotePrecision\":8,\"filterLotSize\":{\"filterType\":\"LOT_SIZE\",\"minQty\":\"0.00000100\",\"maxQty\":\"900.00000000\",\"stepSize\":\"0.00000100\"},\"filterPrice\":{\"filterType\":\"PRICE_FILTER\",\"minPrice\":\"0.01000000\",\"maxPrice\":\"1000000.00000000\",\"tickSize\":\"0.01000000\"},\"filterMinNotional\":{\"filterType\":\"MIN_NOTIONAL\",\"minNotional\":\"10.00000000\",\"applyToMarket\":true,\"avgPriceMins\":5}},\"openOrders\":[{\"symbol\":\"BTCUSDT\",\"orderId\":7010265,\"orderListId\":-1,\"clientOrderId\":\"Gox2Mu7nLcDBeAj6mZFVVU\",\"price\":\"48995.86000000\",\"origQty\":\"0.00042800\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"BUY\",\"stopPrice\":\"48990.97000000\",\"icebergQty\":\"0.00000000\",\"time\":1629594092089,\"updateTime\":1629594092089,\"isWorking\":false,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":48942.03,\"updatedAt\":\"2021-08-22T01:01:32.089Z\",\"differenceToExecute\":-0.09999585223581242,\"differenceToCancel\":0.009993155293375189}],\"action\":\"buy-order-wait\",\"baseAssetBalance\":{\"asset\":\"BTC\",\"free\":\"0.00000100\",\"locked\":\"0.00000000\",\"total\":0.000001,\"estimatedValue\":0.04894203,\"updatedAt\":\"2021-08-22T01:01:32+00:00\",\"isLessThanMinNotionalValue\":true},\"quoteAssetBalance\":{\"asset\":\"USDT\",\"free\":\"378987.63247205\",\"locked\":\"20.97022808\"},\"buy\":{\"currentPrice\":48942.03,\"limitPrice\":48995.866233,\"highestPrice\":49015.11,\"lowestPrice\":48892.71,\"athPrice\":null,\"athRestrictionPrice\":null,\"triggerPrice\":48892.71,\"difference\":0.10087393396684963,\"openOrders\":[{\"symbol\":\"BTCUSDT\",\"orderId\":7010265,\"orderListId\":-1,\"clientOrderId\":\"Gox2Mu7nLcDBeAj6mZFVVU\",\"price\":\"48995.86000000\",\"origQty\":\"0.00042800\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"BUY\",\"stopPrice\":\"48990.97000000\",\"icebergQty\":\"0.00000000\",\"time\":1629594092089,\"updateTime\":1629594092089,\"isWorking\":false,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":48942.03,\"updatedAt\":\"2021-08-22T01:01:32.089Z\",\"differenceToExecute\":-0.09999585223581242,\"differenceToCancel\":0.009993155293375189}],\"processMessage\":\"\",\"updatedAt\":\"2021-08-22T01:04:08.210Z\"},\"sell\":{\"currentPrice\":48942.03,\"limitPrice\":null,\"lastBuyPrice\":null,\"triggerPrice\":null,\"difference\":null,\"stopLossTriggerPrice\":null,\"stopLossDifference\":null,\"currentProfit\":null,\"currentProfitPercentage\":null,\"openOrders\":[],\"processMessage\":\"\",\"updatedAt\":\"2021-08-22T01:04:08.210Z\"},\"order\":{},\"saveToCache\":true}", - "ETHBUSD-processed-data": "{\"symbol\":\"ETHBUSD\",\"isLocked\":false,\"featureToggle\":{\"notifyOrderConfirm\":true,\"notifyDebug\":true,\"notifyOrderExecute\":true},\"lastCandle\":{\"eventType\":\"kline\",\"symbol\":\"ETHBUSD\",\"close\":\"1000.01000000\"},\"accountInfo\":{\"makerCommission\":0,\"takerCommission\":0,\"buyerCommission\":0,\"sellerCommission\":0,\"canTrade\":true,\"canWithdraw\":false,\"canDeposit\":false,\"updateTime\":1630151787045,\"accountType\":\"SPOT\",\"balances\":[{\"asset\":\"BNB\",\"free\":\"0.01000000\",\"locked\":\"0.04000000\",\"total\":0.05,\"estimatedValue\":24.285,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},{\"asset\":\"BTC\",\"free\":\"0.00000100\",\"locked\":\"0.00000000\",\"total\":0.000001,\"estimatedValue\":0.04887893,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":true},{\"asset\":\"BUSD\",\"free\":\"9904.79690824\",\"locked\":\"41.96541778\"},{\"asset\":\"ETH\",\"free\":\"0.10001000\",\"locked\":\"99.90687000\",\"total\":100.00688,\"estimatedValue\":100007.88006879999,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},{\"asset\":\"LTC\",\"free\":\"499.75398000\",\"locked\":\"0.00000000\",\"total\":499.75398,\"estimatedValue\":35192.675271600005,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},{\"asset\":\"TRX\",\"free\":\"500000.00000000\",\"locked\":\"0.00000000\"},{\"asset\":\"USDT\",\"free\":\"379068.42127337\",\"locked\":\"0.00000000\"},{\"asset\":\"XRP\",\"free\":\"50000.00000000\",\"locked\":\"0.00000000\"}],\"permissions\":[\"SPOT\"]},\"symbolConfiguration\":{\"_id\":\"61167428434d24b0b57ce954\",\"key\":\"configuration\",\"enabled\":true,\"cronTime\":\"* * * * * *\",\"symbols\":[\"BTCUSDT\",\"BNBUSDT\",\"LTCBUSD\",\"BTCBUSD\",\"ETHBUSD\"],\"botOptions\":{\"authentication\":{\"lockList\":false,\"lockAfter\":120},\"autoTriggerBuy\":{\"enabled\":true,\"triggerAfter\":1}},\"candles\":{\"interval\":\"1m\",\"limit\":10},\"buy\":{\"enabled\":true,\"lastBuyPriceRemoveThreshold\":10,\"athRestriction\":{\"enabled\":false,\"candles\":{\"interval\":\"1d\",\"limit\":30},\"restrictionPercentage\":0.9},\"maxPurchaseAmount\":-1,\"gridTrade\":[{\"triggerPercentage\":1,\"stopPercentage\":1.001,\"limitPercentage\":1.0011,\"maxPurchaseAmount\":21,\"executed\":true,\"executedOrder\":{\"symbol\":\"ETHBUSD\",\"orderId\":3424,\"orderListId\":-1,\"clientOrderId\":\"PwNd1dkdHiQG4d7vBbhRYp\",\"price\":\"3049.95000000\",\"origQty\":\"0.00688000\",\"executedQty\":\"0.00688000\",\"cummulativeQuoteQty\":\"20.98365600\",\"status\":\"FILLED\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"BUY\",\"stopPrice\":\"3049.64000000\",\"icebergQty\":\"0.00000000\",\"time\":1629425885316,\"updateTime\":1629513026900,\"isWorking\":true,\"origQuoteOrderQty\":\"0.00000000\",\"currentGridTradeIndex\":0,\"nextCheck\":\"2021-08-20T12:40:35.226Z\"}}],\"currentGridTradeIndex\":-1,\"currentGridTrade\":null},\"sell\":{\"enabled\":true,\"stopLoss\":{\"enabled\":true,\"maxLossPercentage\":0.99,\"disableBuyMinutes\":1,\"orderType\":\"market\"},\"gridTrade\":[{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}],\"currentGridTradeIndex\":0,\"currentGridTrade\":{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}},\"system\":{\"temporaryDisableActionAfterConfirmingOrder\":20,\"checkManualBuyOrderPeriod\":5,\"placeManualOrderInterval\":5,\"refreshAccountInfoPeriod\":1,\"checkOrderExecutePeriod\":10,\"checkManualOrderPeriod\":5}},\"indicators\":{\"highestPrice\":1000.01,\"lowestPrice\":1000.01,\"athPrice\":null},\"symbolInfo\":{\"symbol\":\"ETHBUSD\",\"status\":\"TRADING\",\"baseAsset\":\"ETH\",\"baseAssetPrecision\":8,\"quoteAsset\":\"BUSD\",\"quotePrecision\":8,\"filterLotSize\":{\"filterType\":\"LOT_SIZE\",\"minQty\":\"0.00001000\",\"maxQty\":\"9000.00000000\",\"stepSize\":\"0.00001000\"},\"filterPrice\":{\"filterType\":\"PRICE_FILTER\",\"minPrice\":\"0.01000000\",\"maxPrice\":\"100000.00000000\",\"tickSize\":\"0.01000000\"},\"filterMinNotional\":{\"filterType\":\"MIN_NOTIONAL\",\"minNotional\":\"10.00000000\",\"applyToMarket\":true,\"avgPriceMins\":5}},\"openOrders\":[{\"symbol\":\"ETHBUSD\",\"orderId\":3632,\"orderListId\":-1,\"clientOrderId\":\"2jdLRcZMCmjDA9qf1YAeHf\",\"price\":\"3344.21000000\",\"origQty\":\"99.90687000\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"SELL\",\"stopPrice\":\"3347.56000000\",\"icebergQty\":\"0.00000000\",\"time\":1629755493058,\"updateTime\":1629803948834,\"isWorking\":true,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":1000.01,\"updatedAt\":\"2021-08-23T21:51:33.058Z\",\"differenceToExecute\":-234.75265247347528,\"differenceToCancel\":-235.42349947242008,\"minimumProfit\":29398.595566199976,\"minimumProfitPercentage\":9.648027016836336}],\"action\":\"sell-order-wait\",\"baseAssetBalance\":{\"asset\":\"ETH\",\"free\":\"0.10001000\",\"locked\":\"99.90687000\",\"total\":100.00688,\"estimatedValue\":100007.88006879999,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},\"quoteAssetBalance\":{\"asset\":\"BUSD\",\"free\":\"9904.79690824\",\"locked\":\"41.96541778\"},\"buy\":{\"currentPrice\":1000.01,\"limitPrice\":null,\"highestPrice\":1000.01,\"lowestPrice\":1000.01,\"athPrice\":null,\"athRestrictionPrice\":null,\"triggerPrice\":null,\"difference\":null,\"openOrders\":[],\"processMessage\":\"\",\"updatedAt\":\"2021-08-28T12:09:50.060Z\"},\"sell\":{\"currentPrice\":1000.01,\"limitPrice\":998.00998,\"lastBuyPrice\":3049.9500000000003,\"triggerPrice\":3052.99995,\"difference\":-205.2969420305797,\"stopLossTriggerPrice\":3019.4505000000004,\"stopLossDifference\":-201.94203057969426,\"currentProfit\":-205008.10358720005,\"currentProfitPercentage\":-67.2122493811374,\"openOrders\":[{\"symbol\":\"ETHBUSD\",\"orderId\":3632,\"orderListId\":-1,\"clientOrderId\":\"2jdLRcZMCmjDA9qf1YAeHf\",\"price\":\"3344.21000000\",\"origQty\":\"99.90687000\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"SELL\",\"stopPrice\":\"3347.56000000\",\"icebergQty\":\"0.00000000\",\"time\":1629755493058,\"updateTime\":1629803948834,\"isWorking\":true,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":1000.01,\"updatedAt\":\"2021-08-23T21:51:33.058Z\",\"differenceToExecute\":-234.75265247347528,\"differenceToCancel\":-235.42349947242008,\"minimumProfit\":29398.595566199976,\"minimumProfitPercentage\":9.648027016836336}],\"processMessage\":\"\",\"updatedAt\":\"2021-08-28T12:09:50.060Z\"},\"order\":{},\"saveToCache\":true}", - "BTCBUSD-processed-data": "{\"symbol\":\"BTCBUSD\",\"isLocked\":false,\"featureToggle\":{\"notifyOrderConfirm\":true,\"notifyDebug\":true,\"notifyOrderExecute\":true},\"lastCandle\":{\"eventType\":\"kline\",\"symbol\":\"BTCBUSD\",\"close\":\"48878.93000000\"},\"accountInfo\":{\"makerCommission\":0,\"takerCommission\":0,\"buyerCommission\":0,\"sellerCommission\":0,\"canTrade\":true,\"canWithdraw\":false,\"canDeposit\":false,\"updateTime\":1630151787045,\"accountType\":\"SPOT\",\"balances\":[{\"asset\":\"BNB\",\"free\":\"0.01000000\",\"locked\":\"0.04000000\",\"total\":0.05,\"estimatedValue\":24.285,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},{\"asset\":\"BTC\",\"free\":\"0.00000100\",\"locked\":\"0.00000000\",\"total\":0.000001,\"estimatedValue\":0.04887893,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":true},{\"asset\":\"BUSD\",\"free\":\"9904.79690824\",\"locked\":\"41.96541778\"},{\"asset\":\"ETH\",\"free\":\"0.10001000\",\"locked\":\"99.90687000\",\"total\":100.00688,\"estimatedValue\":0,\"updatedAt\":\"2021-08-28T11:56:27+00:00\"},{\"asset\":\"LTC\",\"free\":\"499.75398000\",\"locked\":\"0.00000000\",\"total\":499.75398,\"estimatedValue\":35192.675271600005,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},{\"asset\":\"TRX\",\"free\":\"500000.00000000\",\"locked\":\"0.00000000\"},{\"asset\":\"USDT\",\"free\":\"379068.42127337\",\"locked\":\"0.00000000\"},{\"asset\":\"XRP\",\"free\":\"50000.00000000\",\"locked\":\"0.00000000\"}],\"permissions\":[\"SPOT\"]},\"symbolConfiguration\":{\"_id\":\"61167428434d24b0b57ce954\",\"key\":\"configuration\",\"enabled\":true,\"cronTime\":\"* * * * * *\",\"symbols\":[\"BTCUSDT\",\"BNBUSDT\",\"LTCBUSD\",\"BTCBUSD\",\"ETHBUSD\"],\"botOptions\":{\"authentication\":{\"lockList\":false,\"lockAfter\":120},\"autoTriggerBuy\":{\"enabled\":true,\"triggerAfter\":1}},\"candles\":{\"interval\":\"1m\",\"limit\":10},\"buy\":{\"enabled\":true,\"lastBuyPriceRemoveThreshold\":10,\"athRestriction\":{\"enabled\":false,\"candles\":{\"interval\":\"1d\",\"limit\":30},\"restrictionPercentage\":0.9},\"maxPurchaseAmount\":-1,\"gridTrade\":[{\"triggerPercentage\":1,\"stopPercentage\":1.001,\"limitPercentage\":1.0011,\"maxPurchaseAmount\":21,\"executed\":false,\"executedOrder\":null}],\"currentGridTradeIndex\":0,\"currentGridTrade\":{\"triggerPercentage\":1,\"stopPercentage\":1.001,\"limitPercentage\":1.0011,\"maxPurchaseAmount\":21,\"executed\":false,\"executedOrder\":null}},\"sell\":{\"enabled\":true,\"stopLoss\":{\"enabled\":true,\"maxLossPercentage\":0.99,\"disableBuyMinutes\":1,\"orderType\":\"market\"},\"gridTrade\":[{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}],\"currentGridTradeIndex\":0,\"currentGridTrade\":{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}},\"system\":{\"temporaryDisableActionAfterConfirmingOrder\":20,\"checkManualBuyOrderPeriod\":5,\"placeManualOrderInterval\":5,\"refreshAccountInfoPeriod\":1,\"checkOrderExecutePeriod\":10,\"checkManualOrderPeriod\":5}},\"indicators\":{\"highestPrice\":48878.93,\"lowestPrice\":48878.93,\"athPrice\":null},\"symbolInfo\":{\"symbol\":\"BTCBUSD\",\"status\":\"TRADING\",\"baseAsset\":\"BTC\",\"baseAssetPrecision\":8,\"quoteAsset\":\"BUSD\",\"quotePrecision\":8,\"filterLotSize\":{\"filterType\":\"LOT_SIZE\",\"minQty\":\"0.00000100\",\"maxQty\":\"900.00000000\",\"stepSize\":\"0.00000100\"},\"filterPrice\":{\"filterType\":\"PRICE_FILTER\",\"minPrice\":\"0.01000000\",\"maxPrice\":\"1000000.00000000\",\"tickSize\":\"0.01000000\"},\"filterMinNotional\":{\"filterType\":\"MIN_NOTIONAL\",\"minNotional\":\"10.00000000\",\"applyToMarket\":true,\"avgPriceMins\":5}},\"openOrders\":[{\"symbol\":\"BTCBUSD\",\"orderId\":23261,\"orderListId\":-1,\"clientOrderId\":\"G5BfeMmFcgN8LKX5EIthqB\",\"price\":\"47054.93000000\",\"origQty\":\"0.00044600\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"BUY\",\"stopPrice\":\"47050.23000000\",\"icebergQty\":\"0.00000000\",\"time\":1630126065539,\"updateTime\":1630151787045,\"isWorking\":true,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":48878.93,\"updatedAt\":\"2021-08-28T04:47:45.539Z\",\"differenceToExecute\":3.7412848440012803,\"differenceToCancel\":3.8470530856071328}],\"action\":\"buy-order-wait\",\"baseAssetBalance\":{\"asset\":\"BTC\",\"free\":\"0.00000100\",\"locked\":\"0.00000000\",\"total\":0.000001,\"estimatedValue\":0.04887893,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":true},\"quoteAssetBalance\":{\"asset\":\"BUSD\",\"free\":\"9904.79690824\",\"locked\":\"41.96541778\"},\"buy\":{\"currentPrice\":48878.93,\"limitPrice\":48932.696823000006,\"highestPrice\":48878.93,\"lowestPrice\":48878.93,\"athPrice\":null,\"athRestrictionPrice\":null,\"triggerPrice\":48878.93,\"difference\":0,\"openOrders\":[{\"symbol\":\"BTCBUSD\",\"orderId\":23261,\"orderListId\":-1,\"clientOrderId\":\"G5BfeMmFcgN8LKX5EIthqB\",\"price\":\"47054.93000000\",\"origQty\":\"0.00044600\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"BUY\",\"stopPrice\":\"47050.23000000\",\"icebergQty\":\"0.00000000\",\"time\":1630126065539,\"updateTime\":1630151787045,\"isWorking\":true,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":48878.93,\"updatedAt\":\"2021-08-28T04:47:45.539Z\",\"differenceToExecute\":3.7412848440012803,\"differenceToCancel\":3.8470530856071328}],\"processMessage\":\"\",\"updatedAt\":\"2021-08-28T12:09:50.056Z\"},\"sell\":{\"currentPrice\":48878.93,\"limitPrice\":null,\"lastBuyPrice\":null,\"triggerPrice\":null,\"difference\":null,\"stopLossTriggerPrice\":null,\"stopLossDifference\":null,\"currentProfit\":null,\"currentProfitPercentage\":null,\"openOrders\":[],\"processMessage\":\"\",\"updatedAt\":\"2021-08-28T12:09:50.056Z\"},\"order\":{},\"saveToCache\":true}", - "LTCBUSD-processed-data": "{\"symbol\":\"LTCBUSD\",\"isLocked\":false,\"featureToggle\":{\"notifyOrderConfirm\":true,\"notifyDebug\":true,\"notifyOrderExecute\":true},\"lastCandle\":{\"eventType\":\"kline\",\"symbol\":\"LTCBUSD\",\"close\":\"70.42000000\"},\"accountInfo\":{\"makerCommission\":0,\"takerCommission\":0,\"buyerCommission\":0,\"sellerCommission\":0,\"canTrade\":true,\"canWithdraw\":false,\"canDeposit\":false,\"updateTime\":1630151787045,\"accountType\":\"SPOT\",\"balances\":[{\"asset\":\"BNB\",\"free\":\"0.01000000\",\"locked\":\"0.04000000\",\"total\":0.05,\"estimatedValue\":24.285,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},{\"asset\":\"BTC\",\"free\":\"0.00000100\",\"locked\":\"0.00000000\",\"total\":0.000001,\"estimatedValue\":0,\"updatedAt\":\"2021-08-28T11:56:27+00:00\"},{\"asset\":\"BUSD\",\"free\":\"9904.79690824\",\"locked\":\"41.96541778\"},{\"asset\":\"ETH\",\"free\":\"0.10001000\",\"locked\":\"99.90687000\",\"total\":100.00688,\"estimatedValue\":0,\"updatedAt\":\"2021-08-28T11:56:27+00:00\"},{\"asset\":\"LTC\",\"free\":\"499.75398000\",\"locked\":\"0.00000000\",\"total\":499.75398,\"estimatedValue\":35192.675271600005,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},{\"asset\":\"TRX\",\"free\":\"500000.00000000\",\"locked\":\"0.00000000\"},{\"asset\":\"USDT\",\"free\":\"379068.42127337\",\"locked\":\"0.00000000\"},{\"asset\":\"XRP\",\"free\":\"50000.00000000\",\"locked\":\"0.00000000\"}],\"permissions\":[\"SPOT\"]},\"symbolConfiguration\":{\"_id\":\"61167428434d24b0b57ce954\",\"key\":\"configuration\",\"enabled\":true,\"cronTime\":\"* * * * * *\",\"symbols\":[\"BTCUSDT\",\"BNBUSDT\",\"LTCBUSD\",\"BTCBUSD\",\"ETHBUSD\"],\"botOptions\":{\"authentication\":{\"lockList\":false,\"lockAfter\":120},\"autoTriggerBuy\":{\"enabled\":true,\"triggerAfter\":1}},\"candles\":{\"interval\":\"1m\",\"limit\":10},\"buy\":{\"enabled\":true,\"lastBuyPriceRemoveThreshold\":10,\"athRestriction\":{\"enabled\":false,\"candles\":{\"interval\":\"1d\",\"limit\":30},\"restrictionPercentage\":0.9},\"maxPurchaseAmount\":-1,\"gridTrade\":[{\"triggerPercentage\":1,\"stopPercentage\":1.001,\"limitPercentage\":1.0011,\"maxPurchaseAmount\":21,\"executed\":false,\"executedOrder\":null}],\"currentGridTradeIndex\":0,\"currentGridTrade\":{\"triggerPercentage\":1,\"stopPercentage\":1.001,\"limitPercentage\":1.0011,\"maxPurchaseAmount\":21,\"executed\":false,\"executedOrder\":null}},\"sell\":{\"enabled\":true,\"stopLoss\":{\"enabled\":true,\"maxLossPercentage\":0.99,\"disableBuyMinutes\":1,\"orderType\":\"market\"},\"gridTrade\":[{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}],\"currentGridTradeIndex\":0,\"currentGridTrade\":{\"triggerPercentage\":1.001,\"stopPercentage\":0.999,\"limitPercentage\":0.998,\"quantityPercentage\":1,\"executed\":false,\"executedOrder\":null}},\"system\":{\"temporaryDisableActionAfterConfirmingOrder\":20,\"checkManualBuyOrderPeriod\":5,\"placeManualOrderInterval\":5,\"refreshAccountInfoPeriod\":1,\"checkOrderExecutePeriod\":10,\"checkManualOrderPeriod\":5}},\"indicators\":{\"highestPrice\":70.42,\"lowestPrice\":70.42,\"athPrice\":null},\"symbolInfo\":{\"symbol\":\"LTCBUSD\",\"status\":\"TRADING\",\"baseAsset\":\"LTC\",\"baseAssetPrecision\":8,\"quoteAsset\":\"BUSD\",\"quotePrecision\":8,\"filterLotSize\":{\"filterType\":\"LOT_SIZE\",\"minQty\":\"0.00001000\",\"maxQty\":\"9000.00000000\",\"stepSize\":\"0.00001000\"},\"filterPrice\":{\"filterType\":\"PRICE_FILTER\",\"minPrice\":\"0.01000000\",\"maxPrice\":\"100000.00000000\",\"tickSize\":\"0.01000000\"},\"filterMinNotional\":{\"filterType\":\"MIN_NOTIONAL\",\"minNotional\":\"10.00000000\",\"applyToMarket\":true,\"avgPriceMins\":5}},\"openOrders\":[{\"symbol\":\"LTCBUSD\",\"orderId\":1927,\"orderListId\":-1,\"clientOrderId\":\"JZVoOLOxJ6468XhpkGO0QL\",\"price\":\"70.47000000\",\"origQty\":\"0.29770000\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"BUY\",\"stopPrice\":\"70.47000000\",\"icebergQty\":\"0.00000000\",\"time\":1629614577188,\"updateTime\":1629614577188,\"isWorking\":false,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":70.42,\"updatedAt\":\"2021-08-22T06:42:57.188Z\",\"differenceToExecute\":-0.07100255609202577,\"differenceToCancel\":0.038954593854756414}],\"action\":\"buy-order-wait\",\"baseAssetBalance\":{\"asset\":\"LTC\",\"free\":\"499.75398000\",\"locked\":\"0.00000000\",\"total\":499.75398,\"estimatedValue\":35192.675271600005,\"updatedAt\":\"2021-08-28T11:56:27+00:00\",\"isLessThanMinNotionalValue\":false},\"quoteAssetBalance\":{\"asset\":\"BUSD\",\"free\":\"9904.79690824\",\"locked\":\"41.96541778\"},\"buy\":{\"currentPrice\":70.42,\"limitPrice\":70.49746200000001,\"highestPrice\":70.42,\"lowestPrice\":70.42,\"athPrice\":null,\"athRestrictionPrice\":null,\"triggerPrice\":70.42,\"difference\":0,\"openOrders\":[{\"symbol\":\"LTCBUSD\",\"orderId\":1927,\"orderListId\":-1,\"clientOrderId\":\"JZVoOLOxJ6468XhpkGO0QL\",\"price\":\"70.47000000\",\"origQty\":\"0.29770000\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"BUY\",\"stopPrice\":\"70.47000000\",\"icebergQty\":\"0.00000000\",\"time\":1629614577188,\"updateTime\":1629614577188,\"isWorking\":false,\"origQuoteOrderQty\":\"0.00000000\",\"currentPrice\":70.42,\"updatedAt\":\"2021-08-22T06:42:57.188Z\",\"differenceToExecute\":-0.07100255609202577,\"differenceToCancel\":0.038954593854756414}],\"processMessage\":\"\",\"updatedAt\":\"2021-08-28T12:09:50.051Z\"},\"sell\":{\"currentPrice\":70.42,\"limitPrice\":null,\"lastBuyPrice\":null,\"triggerPrice\":null,\"difference\":null,\"stopLossTriggerPrice\":null,\"stopLossDifference\":null,\"currentProfit\":null,\"currentProfitPercentage\":null,\"openOrders\":[],\"processMessage\":\"\",\"updatedAt\":\"2021-08-28T12:09:50.051Z\"},\"order\":{},\"saveToCache\":true}" -} +[ + { + "symbol": "BNBUSDT", + "isLocked": false, + "featureToggle": { + "notifyOrderConfirm": true, + "notifyDebug": true, + "notifyOrderExecute": true + }, + "lastCandle": { + "eventType": "kline", + "symbol": "BNBUSDT", + "close": "485.70000000" + }, + "symbolConfiguration": { + "_id": "61167428434d24b0b57ce954", + "key": "configuration", + "enabled": true, + "cronTime": "* * * * * *", + "botOptions": { + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 + }, + "buy": { + "enabled": true, + "lastBuyPriceRemoveThreshold": 10, + "athRestriction": { + "enabled": false, + "candles": { + "interval": "1d", + "limit": 30 + }, + "restrictionPercentage": 0.9 + }, + "maxPurchaseAmount": -1, + "gridTrade": [ + { + "triggerPercentage": 1, + "stopPercentage": 1.001, + "limitPercentage": 1.0011, + "maxPurchaseAmount": 21, + "executed": true, + "executedOrder": { + "symbol": "BNBUSDT", + "orderId": 5213213, + "orderListId": -1, + "clientOrderId": "duQYBSnlLOoDgcN3Xa1OsO", + "price": "449.52000000", + "origQty": "0.04000000", + "executedQty": "0.04000000", + "cummulativeQuoteQty": "17.98080000", + "status": "FILLED", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "BUY", + "stopPrice": "449.47000000", + "icebergQty": "0.00000000", + "time": 1629590089149, + "updateTime": 1629590131092, + "isWorking": true, + "origQuoteOrderQty": "0.00000000", + "currentGridTradeIndex": 0, + "nextCheck": "2021-08-21T23:55:33.247Z" + } + } + ], + "currentGridTradeIndex": -1, + "currentGridTrade": null + }, + "sell": { + "enabled": true, + "stopLoss": { + "enabled": true, + "maxLossPercentage": 0.99, + "disableBuyMinutes": 1, + "orderType": "market" + }, + "gridTrade": [ + { + "triggerPercentage": 1.001, + "stopPercentage": 0.999, + "limitPercentage": 0.998, + "quantityPercentage": 1, + "executed": false, + "executedOrder": null + } + ], + "currentGridTradeIndex": 0, + "currentGridTrade": { + "triggerPercentage": 1.001, + "stopPercentage": 0.999, + "limitPercentage": 0.998, + "quantityPercentage": 1, + "executed": false, + "executedOrder": null + } + }, + "system": { + "temporaryDisableActionAfterConfirmingOrder": 20, + "checkManualBuyOrderPeriod": 5, + "placeManualOrderInterval": 5, + "refreshAccountInfoPeriod": 1, + "checkOrderExecutePeriod": 10, + "checkManualOrderPeriod": 5 + } + }, + "indicators": { + "highestPrice": 485.9, + "lowestPrice": 484.1, + "athPrice": null + }, + "symbolInfo": { + "symbol": "BNBUSDT", + "status": "TRADING", + "baseAsset": "BNB", + "baseAssetPrecision": 8, + "quoteAsset": "USDT", + "quotePrecision": 8, + "filterLotSize": { + "filterType": "LOT_SIZE", + "minQty": "0.01000000", + "maxQty": "9000.00000000", + "stepSize": "0.01000000" + }, + "filterPrice": { + "filterType": "PRICE_FILTER", + "minPrice": "0.01000000", + "maxPrice": "10000.00000000", + "tickSize": "0.01000000" + }, + "filterMinNotional": { + "filterType": "MIN_NOTIONAL", + "minNotional": "10.00000000", + "applyToMarket": true, + "avgPriceMins": 5 + } + }, + "openOrders": [ + { + "symbol": "BNBUSDT", + "orderId": 6398822, + "orderListId": -1, + "clientOrderId": "m471GLEsN8YzBDVweWQgN0", + "price": "643.11000000", + "origQty": "0.04000000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "SELL", + "stopPrice": "643.75000000", + "icebergQty": "0.00000000", + "time": 1630129419133, + "updateTime": 1630129428568, + "isWorking": true, + "origQuoteOrderQty": "0.00000000", + "currentPrice": 485.7, + "updatedAt": "2021-08-28T05:43:39.133Z", + "differenceToExecute": -32.54066296067533, + "differenceToCancel": -32.80627551169872, + "minimumProfit": 6.163200000000002, + "minimumProfitPercentage": 31.507269492669177 + } + ], + "action": "sell-order-wait", + "baseAssetBalance": { + "asset": "BNB", + "free": "0.01000000", + "locked": "0.04000000", + "total": 0.05, + "estimatedValue": 24.285, + "updatedAt": "2021-08-28T11:56:27+00:00", + "isLessThanMinNotionalValue": false + }, + "quoteAssetBalance": { + "asset": "USDT", + "free": "379068.42127337", + "locked": "0.00000000" + }, + "buy": { + "currentPrice": 485.7, + "limitPrice": null, + "highestPrice": 485.9, + "lowestPrice": 484.1, + "athPrice": null, + "athRestrictionPrice": null, + "triggerPrice": null, + "difference": null, + "openOrders": [], + "processMessage": "", + "updatedAt": "2021-08-28T12:09:50.048Z" + }, + "sell": { + "currentPrice": 485.7, + "limitPrice": 484.7286, + "lastBuyPrice": 489.03, + "triggerPrice": 489.51902999999993, + "difference": -0.7862940086472925, + "stopLossTriggerPrice": 484.13969999999995, + "stopLossDifference": 0.32124768375541013, + "currentProfit": -0.1664999999999992, + "currentProfitPercentage": -0.6809398196429672, + "openOrders": [ + { + "symbol": "BNBUSDT", + "orderId": 6398822, + "orderListId": -1, + "clientOrderId": "m471GLEsN8YzBDVweWQgN0", + "price": "643.11000000", + "origQty": "0.04000000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "SELL", + "stopPrice": "643.75000000", + "icebergQty": "0.00000000", + "time": 1630129419133, + "updateTime": 1630129428568, + "isWorking": true, + "origQuoteOrderQty": "0.00000000", + "currentPrice": 485.7, + "updatedAt": "2021-08-28T05:43:39.133Z", + "differenceToExecute": -32.54066296067533, + "differenceToCancel": -32.80627551169872, + "minimumProfit": 6.163200000000002, + "minimumProfitPercentage": 31.507269492669177 + } + ], + "processMessage": "", + "updatedAt": "2021-08-28T12:09:50.048Z" + }, + "order": {}, + "saveToCache": true + }, + { + "symbol": "BTCUSDT", + "isLocked": false, + "featureToggle": { + "notifyOrderConfirm": true, + "notifyDebug": true, + "notifyOrderExecute": true + }, + "lastCandle": { + "eventType": "kline", + "symbol": "BTCUSDT", + "close": "48942.03000000" + }, + "symbolConfiguration": { + "_id": "611e5f2dd3630e44f849b05b", + "key": "BTCUSDT-configuration", + "candles": { + "interval": "1m", + "limit": 10 + }, + "buy": { + "enabled": true, + "lastBuyPriceRemoveThreshold": 10, + "athRestriction": { + "enabled": false, + "candles": { + "interval": "1d", + "limit": 30 + }, + "restrictionPercentage": 0.9 + }, + "maxPurchaseAmount": -1, + "gridTrade": [ + { + "triggerPercentage": 1, + "stopPercentage": 1.001, + "limitPercentage": 1.0011, + "maxPurchaseAmount": 21, + "executed": false, + "executedOrder": null + } + ], + "currentGridTradeIndex": 0, + "currentGridTrade": { + "triggerPercentage": 1, + "stopPercentage": 1.001, + "limitPercentage": 1.0011, + "maxPurchaseAmount": 21, + "executed": false, + "executedOrder": null + } + }, + "sell": { + "enabled": true, + "stopLoss": { + "enabled": true, + "maxLossPercentage": 0.99, + "disableBuyMinutes": 1, + "orderType": "market" + }, + "gridTrade": [ + { + "triggerPercentage": 1.001, + "stopPercentage": 0.999, + "limitPercentage": 0.998, + "quantityPercentage": 1, + "executed": false, + "executedOrder": null + } + ], + "currentGridTradeIndex": 0, + "currentGridTrade": { + "triggerPercentage": 1.001, + "stopPercentage": 0.999, + "limitPercentage": 0.998, + "quantityPercentage": 1, + "executed": false, + "executedOrder": null + } + }, + "botOptions": { + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "enabled": true, + "cronTime": "* * * * * *", + "system": { + "temporaryDisableActionAfterConfirmingOrder": 20, + "checkManualBuyOrderPeriod": 5, + "placeManualOrderInterval": 5, + "refreshAccountInfoPeriod": 1, + "checkOrderExecutePeriod": 10, + "checkManualOrderPeriod": 5 + } + }, + "indicators": { + "highestPrice": 49015.11, + "lowestPrice": 48892.71, + "athPrice": null + }, + "symbolInfo": { + "symbol": "BTCUSDT", + "status": "TRADING", + "baseAsset": "BTC", + "baseAssetPrecision": 8, + "quoteAsset": "USDT", + "quotePrecision": 8, + "filterLotSize": { + "filterType": "LOT_SIZE", + "minQty": "0.00000100", + "maxQty": "900.00000000", + "stepSize": "0.00000100" + }, + "filterPrice": { + "filterType": "PRICE_FILTER", + "minPrice": "0.01000000", + "maxPrice": "1000000.00000000", + "tickSize": "0.01000000" + }, + "filterMinNotional": { + "filterType": "MIN_NOTIONAL", + "minNotional": "10.00000000", + "applyToMarket": true, + "avgPriceMins": 5 + } + }, + "openOrders": [ + { + "symbol": "BTCUSDT", + "orderId": 7010265, + "orderListId": -1, + "clientOrderId": "Gox2Mu7nLcDBeAj6mZFVVU", + "price": "48995.86000000", + "origQty": "0.00042800", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "BUY", + "stopPrice": "48990.97000000", + "icebergQty": "0.00000000", + "time": 1629594092089, + "updateTime": 1629594092089, + "isWorking": false, + "origQuoteOrderQty": "0.00000000", + "currentPrice": 48942.03, + "updatedAt": "2021-08-22T01:01:32.089Z", + "differenceToExecute": -0.09999585223581242, + "differenceToCancel": 0.009993155293375189 + } + ], + "action": "buy-order-wait", + "baseAssetBalance": { + "asset": "BTC", + "free": "0.00000100", + "locked": "0.00000000", + "total": 0.000001, + "estimatedValue": 0.04894203, + "updatedAt": "2021-08-22T01:01:32+00:00", + "isLessThanMinNotionalValue": true + }, + "quoteAssetBalance": { + "asset": "USDT", + "free": "378987.63247205", + "locked": "20.97022808" + }, + "buy": { + "currentPrice": 48942.03, + "limitPrice": 48995.866233, + "highestPrice": 49015.11, + "lowestPrice": 48892.71, + "athPrice": null, + "athRestrictionPrice": null, + "triggerPrice": 48892.71, + "difference": 0.10087393396684963, + "openOrders": [ + { + "symbol": "BTCUSDT", + "orderId": 7010265, + "orderListId": -1, + "clientOrderId": "Gox2Mu7nLcDBeAj6mZFVVU", + "price": "48995.86000000", + "origQty": "0.00042800", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "BUY", + "stopPrice": "48990.97000000", + "icebergQty": "0.00000000", + "time": 1629594092089, + "updateTime": 1629594092089, + "isWorking": false, + "origQuoteOrderQty": "0.00000000", + "currentPrice": 48942.03, + "updatedAt": "2021-08-22T01:01:32.089Z", + "differenceToExecute": -0.09999585223581242, + "differenceToCancel": 0.009993155293375189 + } + ], + "processMessage": "", + "updatedAt": "2021-08-22T01:04:08.210Z" + }, + "sell": { + "currentPrice": 48942.03, + "limitPrice": null, + "lastBuyPrice": null, + "triggerPrice": null, + "difference": null, + "stopLossTriggerPrice": null, + "stopLossDifference": null, + "currentProfit": null, + "currentProfitPercentage": null, + "openOrders": [], + "processMessage": "", + "updatedAt": "2021-08-22T01:04:08.210Z" + }, + "order": {}, + "saveToCache": true + }, + { + "symbol": "ETHBUSD", + "isLocked": false, + "featureToggle": { + "notifyOrderConfirm": true, + "notifyDebug": true, + "notifyOrderExecute": true + }, + "lastCandle": { + "eventType": "kline", + "symbol": "ETHBUSD", + "close": "1000.01000000" + }, + "symbolConfiguration": { + "_id": "61167428434d24b0b57ce954", + "key": "configuration", + "enabled": true, + "cronTime": "* * * * * *", + "botOptions": { + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 + }, + "buy": { + "enabled": true, + "lastBuyPriceRemoveThreshold": 10, + "athRestriction": { + "enabled": false, + "candles": { + "interval": "1d", + "limit": 30 + }, + "restrictionPercentage": 0.9 + }, + "maxPurchaseAmount": -1, + "gridTrade": [ + { + "triggerPercentage": 1, + "stopPercentage": 1.001, + "limitPercentage": 1.0011, + "maxPurchaseAmount": 21, + "executed": true, + "executedOrder": { + "symbol": "ETHBUSD", + "orderId": 3424, + "orderListId": -1, + "clientOrderId": "PwNd1dkdHiQG4d7vBbhRYp", + "price": "3049.95000000", + "origQty": "0.00688000", + "executedQty": "0.00688000", + "cummulativeQuoteQty": "20.98365600", + "status": "FILLED", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "BUY", + "stopPrice": "3049.64000000", + "icebergQty": "0.00000000", + "time": 1629425885316, + "updateTime": 1629513026900, + "isWorking": true, + "origQuoteOrderQty": "0.00000000", + "currentGridTradeIndex": 0, + "nextCheck": "2021-08-20T12:40:35.226Z" + } + } + ], + "currentGridTradeIndex": -1, + "currentGridTrade": null + }, + "sell": { + "enabled": true, + "stopLoss": { + "enabled": true, + "maxLossPercentage": 0.99, + "disableBuyMinutes": 1, + "orderType": "market" + }, + "gridTrade": [ + { + "triggerPercentage": 1.001, + "stopPercentage": 0.999, + "limitPercentage": 0.998, + "quantityPercentage": 1, + "executed": false, + "executedOrder": null + } + ], + "currentGridTradeIndex": 0, + "currentGridTrade": { + "triggerPercentage": 1.001, + "stopPercentage": 0.999, + "limitPercentage": 0.998, + "quantityPercentage": 1, + "executed": false, + "executedOrder": null + } + }, + "system": { + "temporaryDisableActionAfterConfirmingOrder": 20, + "checkManualBuyOrderPeriod": 5, + "placeManualOrderInterval": 5, + "refreshAccountInfoPeriod": 1, + "checkOrderExecutePeriod": 10, + "checkManualOrderPeriod": 5 + } + }, + "indicators": { + "highestPrice": 1000.01, + "lowestPrice": 1000.01, + "athPrice": null + }, + "symbolInfo": { + "symbol": "ETHBUSD", + "status": "TRADING", + "baseAsset": "ETH", + "baseAssetPrecision": 8, + "quoteAsset": "BUSD", + "quotePrecision": 8, + "filterLotSize": { + "filterType": "LOT_SIZE", + "minQty": "0.00001000", + "maxQty": "9000.00000000", + "stepSize": "0.00001000" + }, + "filterPrice": { + "filterType": "PRICE_FILTER", + "minPrice": "0.01000000", + "maxPrice": "100000.00000000", + "tickSize": "0.01000000" + }, + "filterMinNotional": { + "filterType": "MIN_NOTIONAL", + "minNotional": "10.00000000", + "applyToMarket": true, + "avgPriceMins": 5 + } + }, + "openOrders": [ + { + "symbol": "ETHBUSD", + "orderId": 3632, + "orderListId": -1, + "clientOrderId": "2jdLRcZMCmjDA9qf1YAeHf", + "price": "3344.21000000", + "origQty": "99.90687000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "SELL", + "stopPrice": "3347.56000000", + "icebergQty": "0.00000000", + "time": 1629755493058, + "updateTime": 1629803948834, + "isWorking": true, + "origQuoteOrderQty": "0.00000000", + "currentPrice": 1000.01, + "updatedAt": "2021-08-23T21:51:33.058Z", + "differenceToExecute": -234.75265247347528, + "differenceToCancel": -235.42349947242008, + "minimumProfit": 29398.595566199976, + "minimumProfitPercentage": 9.648027016836336 + } + ], + "action": "sell-order-wait", + "baseAssetBalance": { + "asset": "ETH", + "free": "0.10001000", + "locked": "99.90687000", + "total": 100.00688, + "estimatedValue": 100007.88006879999, + "updatedAt": "2021-08-28T11:56:27+00:00", + "isLessThanMinNotionalValue": false + }, + "quoteAssetBalance": { + "asset": "BUSD", + "free": "9904.79690824", + "locked": "41.96541778" + }, + "buy": { + "currentPrice": 1000.01, + "limitPrice": null, + "highestPrice": 1000.01, + "lowestPrice": 1000.01, + "athPrice": null, + "athRestrictionPrice": null, + "triggerPrice": null, + "difference": null, + "openOrders": [], + "processMessage": "", + "updatedAt": "2021-08-28T12:09:50.060Z" + }, + "sell": { + "currentPrice": 1000.01, + "limitPrice": 998.00998, + "lastBuyPrice": 3049.9500000000003, + "triggerPrice": 3052.99995, + "difference": -205.2969420305797, + "stopLossTriggerPrice": 3019.4505000000004, + "stopLossDifference": -201.94203057969426, + "currentProfit": -205008.10358720005, + "currentProfitPercentage": -67.2122493811374, + "openOrders": [ + { + "symbol": "ETHBUSD", + "orderId": 3632, + "orderListId": -1, + "clientOrderId": "2jdLRcZMCmjDA9qf1YAeHf", + "price": "3344.21000000", + "origQty": "99.90687000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "SELL", + "stopPrice": "3347.56000000", + "icebergQty": "0.00000000", + "time": 1629755493058, + "updateTime": 1629803948834, + "isWorking": true, + "origQuoteOrderQty": "0.00000000", + "currentPrice": 1000.01, + "updatedAt": "2021-08-23T21:51:33.058Z", + "differenceToExecute": -234.75265247347528, + "differenceToCancel": -235.42349947242008, + "minimumProfit": 29398.595566199976, + "minimumProfitPercentage": 9.648027016836336 + } + ], + "processMessage": "", + "updatedAt": "2021-08-28T12:09:50.060Z" + }, + "order": {}, + "saveToCache": true + }, + { + "symbol": "BTCBUSD", + "isLocked": false, + "featureToggle": { + "notifyOrderConfirm": true, + "notifyDebug": true, + "notifyOrderExecute": true + }, + "lastCandle": { + "eventType": "kline", + "symbol": "BTCBUSD", + "close": "48878.93000000" + }, + "symbolConfiguration": { + "_id": "61167428434d24b0b57ce954", + "key": "configuration", + "enabled": true, + "cronTime": "* * * * * *", + "botOptions": { + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 + }, + "buy": { + "enabled": true, + "lastBuyPriceRemoveThreshold": 10, + "athRestriction": { + "enabled": false, + "candles": { + "interval": "1d", + "limit": 30 + }, + "restrictionPercentage": 0.9 + }, + "maxPurchaseAmount": -1, + "gridTrade": [ + { + "triggerPercentage": 1, + "stopPercentage": 1.001, + "limitPercentage": 1.0011, + "maxPurchaseAmount": 21, + "executed": false, + "executedOrder": null + } + ], + "currentGridTradeIndex": 0, + "currentGridTrade": { + "triggerPercentage": 1, + "stopPercentage": 1.001, + "limitPercentage": 1.0011, + "maxPurchaseAmount": 21, + "executed": false, + "executedOrder": null + } + }, + "sell": { + "enabled": true, + "stopLoss": { + "enabled": true, + "maxLossPercentage": 0.99, + "disableBuyMinutes": 1, + "orderType": "market" + }, + "gridTrade": [ + { + "triggerPercentage": 1.001, + "stopPercentage": 0.999, + "limitPercentage": 0.998, + "quantityPercentage": 1, + "executed": false, + "executedOrder": null + } + ], + "currentGridTradeIndex": 0, + "currentGridTrade": { + "triggerPercentage": 1.001, + "stopPercentage": 0.999, + "limitPercentage": 0.998, + "quantityPercentage": 1, + "executed": false, + "executedOrder": null + } + }, + "system": { + "temporaryDisableActionAfterConfirmingOrder": 20, + "checkManualBuyOrderPeriod": 5, + "placeManualOrderInterval": 5, + "refreshAccountInfoPeriod": 1, + "checkOrderExecutePeriod": 10, + "checkManualOrderPeriod": 5 + } + }, + "indicators": { + "highestPrice": 48878.93, + "lowestPrice": 48878.93, + "athPrice": null + }, + "symbolInfo": { + "symbol": "BTCBUSD", + "status": "TRADING", + "baseAsset": "BTC", + "baseAssetPrecision": 8, + "quoteAsset": "BUSD", + "quotePrecision": 8, + "filterLotSize": { + "filterType": "LOT_SIZE", + "minQty": "0.00000100", + "maxQty": "900.00000000", + "stepSize": "0.00000100" + }, + "filterPrice": { + "filterType": "PRICE_FILTER", + "minPrice": "0.01000000", + "maxPrice": "1000000.00000000", + "tickSize": "0.01000000" + }, + "filterMinNotional": { + "filterType": "MIN_NOTIONAL", + "minNotional": "10.00000000", + "applyToMarket": true, + "avgPriceMins": 5 + } + }, + "openOrders": [ + { + "symbol": "BTCBUSD", + "orderId": 23261, + "orderListId": -1, + "clientOrderId": "G5BfeMmFcgN8LKX5EIthqB", + "price": "47054.93000000", + "origQty": "0.00044600", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "BUY", + "stopPrice": "47050.23000000", + "icebergQty": "0.00000000", + "time": 1630126065539, + "updateTime": 1630151787045, + "isWorking": true, + "origQuoteOrderQty": "0.00000000", + "currentPrice": 48878.93, + "updatedAt": "2021-08-28T04:47:45.539Z", + "differenceToExecute": 3.7412848440012803, + "differenceToCancel": 3.8470530856071328 + } + ], + "action": "buy-order-wait", + "baseAssetBalance": { + "asset": "BTC", + "free": "0.00000100", + "locked": "0.00000000", + "total": 0.000001, + "estimatedValue": 0.04887893, + "updatedAt": "2021-08-28T11:56:27+00:00", + "isLessThanMinNotionalValue": true + }, + "quoteAssetBalance": { + "asset": "BUSD", + "free": "9904.79690824", + "locked": "41.96541778" + }, + "buy": { + "currentPrice": 48878.93, + "limitPrice": 48932.696823000006, + "highestPrice": 48878.93, + "lowestPrice": 48878.93, + "athPrice": null, + "athRestrictionPrice": null, + "triggerPrice": 48878.93, + "difference": 0, + "openOrders": [ + { + "symbol": "BTCBUSD", + "orderId": 23261, + "orderListId": -1, + "clientOrderId": "G5BfeMmFcgN8LKX5EIthqB", + "price": "47054.93000000", + "origQty": "0.00044600", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "BUY", + "stopPrice": "47050.23000000", + "icebergQty": "0.00000000", + "time": 1630126065539, + "updateTime": 1630151787045, + "isWorking": true, + "origQuoteOrderQty": "0.00000000", + "currentPrice": 48878.93, + "updatedAt": "2021-08-28T04:47:45.539Z", + "differenceToExecute": 3.7412848440012803, + "differenceToCancel": 3.8470530856071328 + } + ], + "processMessage": "", + "updatedAt": "2021-08-28T12:09:50.056Z" + }, + "sell": { + "currentPrice": 48878.93, + "limitPrice": null, + "lastBuyPrice": null, + "triggerPrice": null, + "difference": null, + "stopLossTriggerPrice": null, + "stopLossDifference": null, + "currentProfit": null, + "currentProfitPercentage": null, + "openOrders": [], + "processMessage": "", + "updatedAt": "2021-08-28T12:09:50.056Z" + }, + "order": {}, + "saveToCache": true + }, + { + "symbol": "LTCBUSD", + "isLocked": false, + "featureToggle": { + "notifyOrderConfirm": true, + "notifyDebug": true, + "notifyOrderExecute": true + }, + "lastCandle": { + "eventType": "kline", + "symbol": "LTCBUSD", + "close": "70.42000000" + }, + "symbolConfiguration": { + "_id": "61167428434d24b0b57ce954", + "key": "configuration", + "enabled": true, + "cronTime": "* * * * * *", + "botOptions": { + "authentication": { + "lockList": false, + "lockAfter": 120 + }, + "autoTriggerBuy": { + "enabled": true, + "triggerAfter": 1 + } + }, + "candles": { + "interval": "1m", + "limit": 10 + }, + "buy": { + "enabled": true, + "lastBuyPriceRemoveThreshold": 10, + "athRestriction": { + "enabled": false, + "candles": { + "interval": "1d", + "limit": 30 + }, + "restrictionPercentage": 0.9 + }, + "maxPurchaseAmount": -1, + "gridTrade": [ + { + "triggerPercentage": 1, + "stopPercentage": 1.001, + "limitPercentage": 1.0011, + "maxPurchaseAmount": 21, + "executed": false, + "executedOrder": null + } + ], + "currentGridTradeIndex": 0, + "currentGridTrade": { + "triggerPercentage": 1, + "stopPercentage": 1.001, + "limitPercentage": 1.0011, + "maxPurchaseAmount": 21, + "executed": false, + "executedOrder": null + } + }, + "sell": { + "enabled": true, + "stopLoss": { + "enabled": true, + "maxLossPercentage": 0.99, + "disableBuyMinutes": 1, + "orderType": "market" + }, + "gridTrade": [ + { + "triggerPercentage": 1.001, + "stopPercentage": 0.999, + "limitPercentage": 0.998, + "quantityPercentage": 1, + "executed": false, + "executedOrder": null + } + ], + "currentGridTradeIndex": 0, + "currentGridTrade": { + "triggerPercentage": 1.001, + "stopPercentage": 0.999, + "limitPercentage": 0.998, + "quantityPercentage": 1, + "executed": false, + "executedOrder": null + } + }, + "system": { + "temporaryDisableActionAfterConfirmingOrder": 20, + "checkManualBuyOrderPeriod": 5, + "placeManualOrderInterval": 5, + "refreshAccountInfoPeriod": 1, + "checkOrderExecutePeriod": 10, + "checkManualOrderPeriod": 5 + } + }, + "indicators": { + "highestPrice": 70.42, + "lowestPrice": 70.42, + "athPrice": null + }, + "symbolInfo": { + "symbol": "LTCBUSD", + "status": "TRADING", + "baseAsset": "LTC", + "baseAssetPrecision": 8, + "quoteAsset": "BUSD", + "quotePrecision": 8, + "filterLotSize": { + "filterType": "LOT_SIZE", + "minQty": "0.00001000", + "maxQty": "9000.00000000", + "stepSize": "0.00001000" + }, + "filterPrice": { + "filterType": "PRICE_FILTER", + "minPrice": "0.01000000", + "maxPrice": "100000.00000000", + "tickSize": "0.01000000" + }, + "filterMinNotional": { + "filterType": "MIN_NOTIONAL", + "minNotional": "10.00000000", + "applyToMarket": true, + "avgPriceMins": 5 + } + }, + "openOrders": [ + { + "symbol": "LTCBUSD", + "orderId": 1927, + "orderListId": -1, + "clientOrderId": "JZVoOLOxJ6468XhpkGO0QL", + "price": "70.47000000", + "origQty": "0.29770000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "BUY", + "stopPrice": "70.47000000", + "icebergQty": "0.00000000", + "time": 1629614577188, + "updateTime": 1629614577188, + "isWorking": false, + "origQuoteOrderQty": "0.00000000", + "currentPrice": 70.42, + "updatedAt": "2021-08-22T06:42:57.188Z", + "differenceToExecute": -0.07100255609202577, + "differenceToCancel": 0.038954593854756414 + } + ], + "action": "buy-order-wait", + "baseAssetBalance": { + "asset": "LTC", + "free": "499.75398000", + "locked": "0.00000000", + "total": 499.75398, + "estimatedValue": 35192.675271600005, + "updatedAt": "2021-08-28T11:56:27+00:00", + "isLessThanMinNotionalValue": false + }, + "quoteAssetBalance": { + "asset": "BUSD", + "free": "9904.79690824", + "locked": "41.96541778" + }, + "buy": { + "currentPrice": 70.42, + "limitPrice": 70.49746200000001, + "highestPrice": 70.42, + "lowestPrice": 70.42, + "athPrice": null, + "athRestrictionPrice": null, + "triggerPrice": 70.42, + "difference": 0, + "openOrders": [ + { + "symbol": "LTCBUSD", + "orderId": 1927, + "orderListId": -1, + "clientOrderId": "JZVoOLOxJ6468XhpkGO0QL", + "price": "70.47000000", + "origQty": "0.29770000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "STOP_LOSS_LIMIT", + "side": "BUY", + "stopPrice": "70.47000000", + "icebergQty": "0.00000000", + "time": 1629614577188, + "updateTime": 1629614577188, + "isWorking": false, + "origQuoteOrderQty": "0.00000000", + "currentPrice": 70.42, + "updatedAt": "2021-08-22T06:42:57.188Z", + "differenceToExecute": -0.07100255609202577, + "differenceToCancel": 0.038954593854756414 + } + ], + "processMessage": "", + "updatedAt": "2021-08-28T12:09:50.051Z" + }, + "sell": { + "currentPrice": 70.42, + "limitPrice": null, + "lastBuyPrice": null, + "triggerPrice": null, + "difference": null, + "stopLossTriggerPrice": null, + "stopLossDifference": null, + "currentProfit": null, + "currentProfitPercentage": null, + "openOrders": [], + "processMessage": "", + "updatedAt": "2021-08-28T12:09:50.051Z" + }, + "order": {}, + "saveToCache": true + } +] \ No newline at end of file diff --git a/app/frontend/websocket/handlers/__tests__/fixtures/latest-trailing-trade-tradingview.json b/app/frontend/websocket/handlers/__tests__/fixtures/latest-trailing-trade-tradingview.json new file mode 100644 index 00000000..1dbbd628 --- /dev/null +++ b/app/frontend/websocket/handlers/__tests__/fixtures/latest-trailing-trade-tradingview.json @@ -0,0 +1,3 @@ +{ + "BTCUSDT": "{\"request\":{\"symbol\":\"BTCUSDT\",\"screener\":\"CRYPTO\",\"exchange\":\"BINANCE\",\"interval\":\"1h\"},\"result\":{\"indicators\":{\"summary\":{\"BUY\":14,\"NEUTRAL\":10,\"RECOMMENDATION\":\"BUY\",\"SELL\":2},\"time\":\"2022-07-18T11:45:39.308508\"}}}" +} diff --git a/app/frontend/websocket/handlers/__tests__/index.test.js b/app/frontend/websocket/handlers/__tests__/index.test.js index 7b74dbc1..0848de78 100644 --- a/app/frontend/websocket/handlers/__tests__/index.test.js +++ b/app/frontend/websocket/handlers/__tests__/index.test.js @@ -17,7 +17,8 @@ describe('index', () => { handleManualTrade: expect.any(Function), handleCancelOrder: expect.any(Function), handleDustTransferGet: expect.any(Function), - handleDustTransferExecute: expect.any(Function) + handleDustTransferExecute: expect.any(Function), + handleExchangeSymbolsGet: expect.any(Function) }); }); }); diff --git a/app/frontend/websocket/handlers/__tests__/latest.test.js b/app/frontend/websocket/handlers/__tests__/latest.test.js index 8130d6a7..326bd3b6 100644 --- a/app/frontend/websocket/handlers/__tests__/latest.test.js +++ b/app/frontend/websocket/handlers/__tests__/latest.test.js @@ -1,6 +1,7 @@ /* eslint-disable global-require */ describe('latest.test.js', () => { const trailingTradeCommonJson = require('./fixtures/latest-trailing-trade-common.json'); + const trailingTradeTradingView = require('./fixtures/latest-trailing-trade-tradingview.json'); const trailingTradeSymbols = require('./fixtures/latest-trailing-trade-symbols.json'); const trailingTradeClosedTrades = require('./fixtures/latest-trailing-trade-closed-trades.json'); @@ -9,18 +10,20 @@ describe('latest.test.js', () => { const trailingTradeStatsAuthenticated = require('./fixtures/latest-stats-authenticated.json'); - let mockFindOne; + let mockGetCacheTrailingTradeSymbols; + let mockGetCacheTrailingTradeTotalProfitAndLoss; + let mockGetCacheTrailingTradeQuoteEstimates; let mockWebSocketServer; let mockWebSocketServerWebSocketSend; let mockConfigGet; + let mockCacheHGet; let mockCacheHGetAll; - let mockCacheGetWithTTL; - let mockMongoUpsertOne; let mockPubSubPublish; let mockBinanceClientGetInfo; + let mockIsActionDisabled; let mockGetConfiguration; beforeEach(() => { @@ -34,6 +37,18 @@ describe('latest.test.js', () => { }); mockWebSocketServerWebSocketSend = jest.fn().mockResolvedValue(true); + mockGetCacheTrailingTradeSymbols = jest + .fn() + .mockResolvedValue(trailingTradeSymbols); + + mockGetCacheTrailingTradeTotalProfitAndLoss = jest + .fn() + .mockResolvedValue([]); + + mockGetCacheTrailingTradeQuoteEstimates = jest.fn().mockResolvedValue([]); + + mockCacheHGet = jest.fn().mockResolvedValue(6); + mockWebSocketServer = { send: mockWebSocketServerWebSocketSend }; @@ -68,8 +83,35 @@ describe('latest.test.js', () => { }); mockGetConfiguration = jest.fn().mockResolvedValue({ - enabled: true + enabled: true, + symbols: ['BTCUSDT', 'BNBUSDT'] + }); + + mockIsActionDisabled = jest.fn().mockImplementation(symbol => { + if (symbol === 'BNBUSDT') { + return { + isDisabled: true, + ttl: 330, + disabledBy: 'stop loss', + canResume: true, + message: 'Temporary disabled by stop loss' + }; + } + + return { + isDisabled: false, + ttl: -2 + }; }); + + jest.mock('../../../../cronjob/trailingTradeHelper/common', () => ({ + isActionDisabled: mockIsActionDisabled, + getCacheTrailingTradeSymbols: mockGetCacheTrailingTradeSymbols, + getCacheTrailingTradeTotalProfitAndLoss: + mockGetCacheTrailingTradeTotalProfitAndLoss, + getCacheTrailingTradeQuoteEstimates: + mockGetCacheTrailingTradeQuoteEstimates + })); }); describe('when some cache is invalid', () => { @@ -91,12 +133,9 @@ describe('latest.test.js', () => { debug: jest.fn(), child: jest.fn() }, - mongo: { - findOne: mockFindOne, - upsertOne: mockMongoUpsertOne - }, cache: { - hgetall: mockCacheHGetAll + hgetall: mockCacheHGetAll, + hget: mockCacheHGet }, config: { get: mockConfigGet @@ -114,7 +153,14 @@ describe('latest.test.js', () => { const { logger } = require('../../../../helpers'); const { handleLatest } = require('../latest'); - await handleLatest(logger, mockWebSocketServer, {}); + await handleLatest(logger, mockWebSocketServer, { + data: { + sortBy: 'default', + sortByDesc: false, + page: 1, + searchKeyword: '' + } + }); }); it('does not trigger ws.send', () => { @@ -131,41 +177,25 @@ describe('latest.test.js', () => { return trailingTradeCommonJson; } - if (pattern === 'trailing-trade-symbols:*-processed-data') { - return trailingTradeSymbols; - } - if (pattern === 'trailing-trade-closed-trades:*') { return trailingTradeClosedTrades; } + if (pattern === 'trailing-trade-tradingview:*') { + return trailingTradeTradingView; + } + return ''; }); - mockCacheGetWithTTL = jest.fn().mockImplementation(key => { - if (key === 'BNBUSDT-disable-action') { - return [ - [null, 330], - [ - null, - JSON.stringify({ - disabledBy: 'stop loss', - canResume: true, - message: 'Temporary disabled by stop loss' - }) - ] - ]; + mockGetCacheTrailingTradeQuoteEstimates = jest.fn().mockResolvedValue([ + { + baseAsset: 'ETH', + estimatedValue: '1574.50', + quoteAsset: 'USDT', + tickSize: '0.01000000' } - - if (key === 'ETHUSDT-disable-action') { - return [ - [null, -2], - [null, null] - ]; - } - - return null; - }); + ]); }); describe('not authenticated and locked list', () => { @@ -173,6 +203,7 @@ describe('latest.test.js', () => { mockGetConfiguration = jest.fn().mockResolvedValue({ enabled: true, type: 'i-am-global', + symbols: ['BTCUSDT', 'BNBUSDT'], candles: { interval: '15m' }, botOptions: { authentication: { @@ -223,7 +254,13 @@ describe('latest.test.js', () => { const { logger } = require('../../../../helpers'); const { handleLatest } = require('../latest'); await handleLatest(logger, mockWebSocketServer, { - isAuthenticated: false + isAuthenticated: false, + data: { + sortBy: 'default', + sortByDesc: false, + page: 1, + searchKeyword: '' + } }); }); @@ -257,6 +294,7 @@ describe('latest.test.js', () => { mockGetConfiguration = jest.fn().mockResolvedValue({ enabled: true, type: 'i-am-global', + symbols: ['BTCUSDT', 'BNBUSDT', 'ETHBUSD', 'BTCBUSD', 'LTCBUSD'], candles: { interval: '15m' }, botOptions: { authentication: { @@ -293,7 +331,7 @@ describe('latest.test.js', () => { }, cache: { hgetall: mockCacheHGetAll, - getWithTTL: mockCacheGetWithTTL + hget: mockCacheHGet }, config: { get: mockConfigGet @@ -308,7 +346,13 @@ describe('latest.test.js', () => { const { logger } = require('../../../../helpers'); const { handleLatest } = require('../latest'); await handleLatest(logger, mockWebSocketServer, { - isAuthenticated: false + isAuthenticated: false, + data: { + sortBy: 'default', + sortByDesc: false, + page: 1, + searchKeyword: '' + } }); }); @@ -317,6 +361,7 @@ describe('latest.test.js', () => { require('../../../../../package.json').version; trailingTradeStateNotAuthenticatedUnlockList.common.gitHash = 'some-hash'; + expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify(trailingTradeStateNotAuthenticatedUnlockList) ); @@ -329,6 +374,7 @@ describe('latest.test.js', () => { enabled: true, type: 'i-am-global', candles: { interval: '15m' }, + symbols: ['BTCUSDT', 'BNBUSDT', 'ETHBUSD', 'BTCBUSD', 'LTCBUSD'], botOptions: { authentication: { lockList: true, @@ -364,7 +410,7 @@ describe('latest.test.js', () => { }, cache: { hgetall: mockCacheHGetAll, - getWithTTL: mockCacheGetWithTTL + hget: mockCacheHGet }, config: { get: mockConfigGet @@ -379,7 +425,13 @@ describe('latest.test.js', () => { const { logger } = require('../../../../helpers'); const { handleLatest } = require('../latest'); await handleLatest(logger, mockWebSocketServer, { - isAuthenticated: true + isAuthenticated: true, + data: { + sortBy: 'default', + sortByDesc: false, + page: 1, + searchKeyword: '' + } }); }); @@ -387,6 +439,87 @@ describe('latest.test.js', () => { trailingTradeStatsAuthenticated.common.version = require('../../../../../package.json').version; trailingTradeStatsAuthenticated.common.gitHash = 'some-hash'; + + expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( + JSON.stringify(trailingTradeStatsAuthenticated) + ); + }); + }); + + describe('authenticated and no git hash provided', () => { + beforeEach(async () => { + delete process.env.GIT_HASH; + + mockGetConfiguration = jest.fn().mockResolvedValue({ + enabled: true, + type: 'i-am-global', + candles: { interval: '15m' }, + symbols: ['BTCUSDT', 'BNBUSDT', 'ETHBUSD', 'BTCBUSD', 'LTCBUSD'], + botOptions: { + authentication: { + lockList: true, + lockAfter: 120 + }, + autoTriggerBuy: { + enabled: false, + triggerAfter: 20 + }, + orderLimit: { + enabled: true, + maxBuyOpenOrders: 3, + maxOpenTrades: 5 + } + }, + sell: {} + }); + + jest.mock( + '../../../../cronjob/trailingTradeHelper/configuration', + () => ({ + getConfiguration: mockGetConfiguration + }) + ); + + jest.mock('../../../../helpers', () => ({ + logger: { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + child: jest.fn() + }, + cache: { + hgetall: mockCacheHGetAll, + hget: mockCacheHGet + }, + config: { + get: mockConfigGet + }, + binance: { + client: { + getInfo: mockBinanceClientGetInfo + } + } + })); + + const { logger } = require('../../../../helpers'); + const { handleLatest } = require('../latest'); + await handleLatest(logger, mockWebSocketServer, { + isAuthenticated: true, + data: { + sortBy: 'default', + sortByDesc: false, + page: 1, + searchKeyword: '' + } + }); + }); + + it('triggers ws.send with latest', () => { + trailingTradeStatsAuthenticated.common.version = + require('../../../../../package.json').version; + trailingTradeStatsAuthenticated.common.gitHash = 'unspecified'; + expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify(trailingTradeStatsAuthenticated) ); diff --git a/app/frontend/websocket/handlers/__tests__/manual-trade-all-symbols.test.js b/app/frontend/websocket/handlers/__tests__/manual-trade-all-symbols.test.js index c1b77f90..6cb63d4a 100644 --- a/app/frontend/websocket/handlers/__tests__/manual-trade-all-symbols.test.js +++ b/app/frontend/websocket/handlers/__tests__/manual-trade-all-symbols.test.js @@ -11,6 +11,7 @@ describe('manual-trade-all-symbols.js', () => { let mockGetGlobalConfiguration; let mockSaveOverrideAction; + let mockExecuteTrailingTrade; const orders = { side: 'buy', @@ -236,6 +237,12 @@ describe('manual-trade-all-symbols.js', () => { jest.mock('../../../../cronjob/trailingTradeHelper/common', () => ({ saveOverrideAction: mockSaveOverrideAction })); + + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + + jest.mock('../../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); }); beforeEach(async () => { @@ -274,46 +281,57 @@ describe('manual-trade-all-symbols.js', () => { }); }); - _.forOwn(orders.buy.symbols, (quoteAsset, _quoteSymbol) => { - _.forOwn(quoteAsset.baseAssets, (baseAsset, _baseSymbol) => { - const { symbol } = baseAsset; - const quoteOrderQty = parseFloat(baseAsset.quoteOrderQty); - - if (quoteOrderQty > 0) { - it('triggers saveOverrideAction', () => { - expect(mockSaveOverrideAction).toHaveBeenCalledWith( - loggerMock, - symbol, - { - action: 'manual-trade', - order: { - side: 'buy', - buy: { - type: 'market', - marketType: 'total', - quoteOrderQty - } - }, - actionAt: expect.any(String), - triggeredBy: 'user' - }, - `Order for ${symbol} has been queued.` - ); - }); - } else { - it('does not trigger saveOverrideAction', () => { - // Get all symbols called with cache.hset - const symbols = _.reduce( - mockSaveOverrideAction.mock.calls, - (newSymbols, s) => { - newSymbols.push(s[1]); - return newSymbols; - }, - [] - ); - expect(symbols).not.toContain(symbol); + _.forOwn(orders.buy.symbols, (quoteAsset, quoteSymbol) => { + describe(`quote symbol - ${quoteSymbol}`, () => { + _.forOwn(quoteAsset.baseAssets, (baseAsset, baseSymbol) => { + const { symbol } = baseAsset; + const quoteOrderQty = parseFloat(baseAsset.quoteOrderQty); + + describe(`base symbol - ${baseSymbol}`, () => { + if (quoteOrderQty > 0) { + it('triggers saveOverrideAction', () => { + expect(mockSaveOverrideAction).toHaveBeenCalledWith( + loggerMock, + symbol, + { + action: 'manual-trade', + order: { + side: 'buy', + buy: { + type: 'market', + marketType: 'total', + quoteOrderQty + } + }, + actionAt: expect.any(String), + triggeredBy: 'user' + }, + `Order for ${symbol} has been queued.` + ); + }); + + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + loggerMock, + symbol + ); + }); + } else { + it('does not trigger saveOverrideAction', () => { + // Get all symbols called with cache.hset + const symbols = _.reduce( + mockSaveOverrideAction.mock.calls, + (newSymbols, s) => { + newSymbols.push(s[1]); + return newSymbols; + }, + [] + ); + expect(symbols).not.toContain(symbol); + }); + } }); - } + }); }); }); @@ -363,46 +381,57 @@ describe('manual-trade-all-symbols.js', () => { }); }); - _.forOwn(orders.sell.symbols, (quoteAsset, _quoteSymbol) => { - _.forOwn(quoteAsset.baseAssets, (baseAsset, _baseSymbol) => { - const { symbol } = baseAsset; - const marketQuantity = parseFloat(baseAsset.marketQuantity); - - if (marketQuantity > 0) { - it('triggers saveOverrideAction', () => { - expect(mockSaveOverrideAction).toHaveBeenCalledWith( - loggerMock, - symbol, - { - action: 'manual-trade', - order: { - side: 'sell', - sell: { - type: 'market', - marketType: 'amount', - marketQuantity - } - }, - actionAt: expect.any(String), - triggeredBy: 'user' - }, - `Order for ${symbol} has been queued.` - ); - }); - } else { - it('does not trigger saveOverrideAction', () => { - // Get all symbols called with cache.hset - const symbols = _.reduce( - mockSaveOverrideAction.mock.calls, - (newSymbols, s) => { - newSymbols.push(s[1]); - return newSymbols; - }, - [] - ); - expect(symbols).not.toContain(symbol); + _.forOwn(orders.sell.symbols, (quoteAsset, quoteSymbol) => { + describe(`quote symbol - ${quoteSymbol}`, () => { + _.forOwn(quoteAsset.baseAssets, (baseAsset, baseSymbol) => { + const { symbol } = baseAsset; + const marketQuantity = parseFloat(baseAsset.marketQuantity); + + describe(`base symbol - ${baseSymbol}`, () => { + if (marketQuantity > 0) { + it('triggers saveOverrideAction', () => { + expect(mockSaveOverrideAction).toHaveBeenCalledWith( + loggerMock, + symbol, + { + action: 'manual-trade', + order: { + side: 'sell', + sell: { + type: 'market', + marketType: 'amount', + marketQuantity + } + }, + actionAt: expect.any(String), + triggeredBy: 'user' + }, + `Order for ${symbol} has been queued.` + ); + }); + + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); + } else { + it('does not trigger saveOverrideAction', () => { + // Get all symbols called with cache.hset + const symbols = _.reduce( + mockSaveOverrideAction.mock.calls, + (newSymbols, s) => { + newSymbols.push(s[1]); + return newSymbols; + }, + [] + ); + expect(symbols).not.toContain(symbol); + }); + } }); - } + }); }); }); diff --git a/app/frontend/websocket/handlers/__tests__/manual-trade.test.js b/app/frontend/websocket/handlers/__tests__/manual-trade.test.js index b3c58cc2..cad37129 100644 --- a/app/frontend/websocket/handlers/__tests__/manual-trade.test.js +++ b/app/frontend/websocket/handlers/__tests__/manual-trade.test.js @@ -6,6 +6,7 @@ describe('manual-trade.js', () => { let loggerMock; let mockSaveOverrideAction; + let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -21,6 +22,12 @@ describe('manual-trade.js', () => { jest.mock('../../../../cronjob/trailingTradeHelper/common', () => ({ saveOverrideAction: mockSaveOverrideAction })); + + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + + jest.mock('../../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); }); beforeEach(async () => { @@ -55,6 +62,13 @@ describe('manual-trade.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify({ diff --git a/app/frontend/websocket/handlers/__tests__/symbol-enable-action.test.js b/app/frontend/websocket/handlers/__tests__/symbol-enable-action.test.js index a6835fe2..d753e198 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-enable-action.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-enable-action.test.js @@ -7,6 +7,7 @@ describe('symbol-enable-action.test.js', () => { let mockLogger; let mockDeleteDisableAction; + let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -16,6 +17,12 @@ describe('symbol-enable-action.test.js', () => { mockWebSocketServer = { send: mockWebSocketServerWebSocketSend }; + + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + + jest.mock('../../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); }); describe('when symbol is provided', () => { @@ -44,6 +51,13 @@ describe('symbol-enable-action.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + mockLogger, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify({ diff --git a/app/frontend/websocket/handlers/__tests__/symbol-grid-trade-delete.test.js b/app/frontend/websocket/handlers/__tests__/symbol-grid-trade-delete.test.js index d384f3a5..e509c601 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-grid-trade-delete.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-grid-trade-delete.test.js @@ -10,6 +10,8 @@ describe('symbol-grid-trade-delete.test.js', () => { let mockArchiveSymbolGridTrade; let mockDeleteSymbolGridTrade; + let mockExecuteTrailingTrade; + beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -25,6 +27,12 @@ describe('symbol-grid-trade-delete.test.js', () => { mockWebSocketServer = { send: mockWebSocketServerWebSocketSend }; + + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + + jest.mock('../../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); }); describe('when symbol is provided', () => { @@ -83,6 +91,13 @@ describe('symbol-grid-trade-delete.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + mockLogger, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify({ @@ -148,6 +163,13 @@ describe('symbol-grid-trade-delete.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + mockLogger, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify({ @@ -206,6 +228,13 @@ describe('symbol-grid-trade-delete.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + mockLogger, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify({ @@ -261,6 +290,13 @@ describe('symbol-grid-trade-delete.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + mockLogger, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify({ diff --git a/app/frontend/websocket/handlers/__tests__/symbol-setting-delete.test.js b/app/frontend/websocket/handlers/__tests__/symbol-setting-delete.test.js index 2b208623..57cda4e3 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-setting-delete.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-setting-delete.test.js @@ -8,6 +8,8 @@ describe('symbol-setting-delete.test.js', () => { let mockDeleteSymbolConfiguration; + let mockExecuteTrailingTrade; + beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -32,6 +34,12 @@ describe('symbol-setting-delete.test.js', () => { }) ); + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + + jest.mock('../../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); + const { handleSymbolSettingDelete } = require('../symbol-setting-delete'); await handleSymbolSettingDelete(logger, mockWebSocketServer, { data: { @@ -47,6 +55,13 @@ describe('symbol-setting-delete.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + mockLogger, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify({ diff --git a/app/frontend/websocket/handlers/__tests__/symbol-setting-update.test.js b/app/frontend/websocket/handlers/__tests__/symbol-setting-update.test.js index e2e98fcf..76b4e90b 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-setting-update.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-setting-update.test.js @@ -8,6 +8,7 @@ describe('symbol-setting-update.test.js', () => { let mockGetSymbolConfiguration; let mockSaveSymbolConfiguration; + let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -17,6 +18,12 @@ describe('symbol-setting-update.test.js', () => { mockWebSocketServer = { send: mockWebSocketServerWebSocketSend }; + + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + + jest.mock('../../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); }); describe('when configuration is valid', () => { @@ -258,6 +265,13 @@ describe('symbol-setting-update.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + mockLogger, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { const args = JSON.parse( mockWebSocketServerWebSocketSend.mock.calls[0][0] diff --git a/app/frontend/websocket/handlers/__tests__/symbol-trigger-buy.test.js b/app/frontend/websocket/handlers/__tests__/symbol-trigger-buy.test.js index 81607f97..3313d9be 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-trigger-buy.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-trigger-buy.test.js @@ -7,6 +7,7 @@ describe('symbol-trigger-buy.test.js', () => { let mockLogger; let mockSaveOverrideAction; + let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -22,6 +23,12 @@ describe('symbol-trigger-buy.test.js', () => { jest.mock('../../../../cronjob/trailingTradeHelper/common', () => ({ saveOverrideAction: mockSaveOverrideAction })); + + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + + jest.mock('../../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); }); describe('when symbol is provided', () => { @@ -52,6 +59,13 @@ describe('symbol-trigger-buy.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + mockLogger, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify({ diff --git a/app/frontend/websocket/handlers/__tests__/symbol-trigger-sell.test.js b/app/frontend/websocket/handlers/__tests__/symbol-trigger-sell.test.js index a9852bb8..ebde0721 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-trigger-sell.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-trigger-sell.test.js @@ -7,6 +7,7 @@ describe('symbol-trigger-sell.test.js', () => { let mockLogger; let mockSaveOverrideAction; + let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -22,6 +23,12 @@ describe('symbol-trigger-sell.test.js', () => { jest.mock('../../../../cronjob/trailingTradeHelper/common', () => ({ saveOverrideAction: mockSaveOverrideAction })); + + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + + jest.mock('../../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); }); describe('when symbol is provided', () => { @@ -50,6 +57,13 @@ describe('symbol-trigger-sell.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + mockLogger, + 'BTCUSDT' + ); + }); + it('triggers ws.send', () => { expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( JSON.stringify({ diff --git a/app/frontend/websocket/handlers/__tests__/symbol-update-last-buy-price.test.js b/app/frontend/websocket/handlers/__tests__/symbol-update-last-buy-price.test.js index 868f5f6a..0dc07c62 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-update-last-buy-price.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-update-last-buy-price.test.js @@ -6,6 +6,7 @@ describe('symbol-update-last-buy-price.test.js', () => { let mockGetAccountInfo; let mockSaveLastBuyPrice; + let mockExecuteTrailingTrade; let loggerMock; let mongoMock; @@ -20,6 +21,12 @@ describe('symbol-update-last-buy-price.test.js', () => { mockWebSocketServer = { send: mockWebSocketServerWebSocketSend }; + + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + + jest.mock('../../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); }); describe('update symbol last buy price', () => { @@ -54,6 +61,13 @@ describe('symbol-update-last-buy-price.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); + it('triggers PubSub.publish', () => { expect(PubSubMock.publish).toHaveBeenCalledWith( 'frontend-notification', @@ -199,6 +213,13 @@ describe('symbol-update-last-buy-price.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); + it('triggers PubSub.publish', () => { expect(PubSubMock.publish).toHaveBeenCalledWith( 'frontend-notification', @@ -286,6 +307,13 @@ describe('symbol-update-last-buy-price.test.js', () => { ); }); + it('triggers executeTrailingTrade', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUSDT' + ); + }); + it('triggers PubSub.publish', () => { expect(PubSubMock.publish).toHaveBeenCalledWith( 'frontend-notification', diff --git a/app/frontend/websocket/handlers/cancel-order.js b/app/frontend/websocket/handlers/cancel-order.js index f1b49bac..6fb75616 100644 --- a/app/frontend/websocket/handlers/cancel-order.js +++ b/app/frontend/websocket/handlers/cancel-order.js @@ -2,6 +2,7 @@ const moment = require('moment'); const { saveOverrideAction } = require('../../../cronjob/trailingTradeHelper/common'); +const { executeTrailingTrade } = require('../../../cronjob'); const handleCancelOrder = async (logger, ws, payload) => { logger.info({ payload }, 'Start cancel order'); @@ -22,6 +23,8 @@ const handleCancelOrder = async (logger, ws, payload) => { 'Cancelling the order action has been received. Wait for cancelling the order.' ); + executeTrailingTrade(logger, symbol); + ws.send( JSON.stringify({ result: true, diff --git a/app/frontend/websocket/handlers/exchange-symbols-get.js b/app/frontend/websocket/handlers/exchange-symbols-get.js new file mode 100644 index 00000000..840f65ef --- /dev/null +++ b/app/frontend/websocket/handlers/exchange-symbols-get.js @@ -0,0 +1,18 @@ +const { cache } = require('../../../helpers'); + +const handleExchangeSymbolsGet = async (_logger, ws, _payload) => { + // Get cached exchange symbols + const exchangeSymbols = + JSON.parse(await cache.hget('trailing-trade-common', 'exchange-symbols')) || + {}; + + ws.send( + JSON.stringify({ + result: true, + type: 'exchange-symbols-get-result', + exchangeSymbols + }) + ); +}; + +module.exports = { handleExchangeSymbolsGet }; diff --git a/app/frontend/websocket/handlers/index.js b/app/frontend/websocket/handlers/index.js index 8f3e8291..f58fe630 100644 --- a/app/frontend/websocket/handlers/index.js +++ b/app/frontend/websocket/handlers/index.js @@ -15,6 +15,7 @@ const { handleManualTradeAllSymbols } = require('./manual-trade-all-symbols'); const { handleCancelOrder } = require('./cancel-order'); const { handleDustTransferGet } = require('./dust-transfer-get'); const { handleDustTransferExecute } = require('./dust-transfer-execute'); +const { handleExchangeSymbolsGet } = require('./exchange-symbols-get'); module.exports = { handleLatest, @@ -31,5 +32,6 @@ module.exports = { handleManualTradeAllSymbols, handleCancelOrder, handleDustTransferGet, - handleDustTransferExecute + handleDustTransferExecute, + handleExchangeSymbolsGet }; diff --git a/app/frontend/websocket/handlers/latest.js b/app/frontend/websocket/handlers/latest.js index a722f6fe..808db6e1 100644 --- a/app/frontend/websocket/handlers/latest.js +++ b/app/frontend/websocket/handlers/latest.js @@ -1,5 +1,4 @@ const _ = require('lodash'); - const { version } = require('../../../../package.json'); const { binance, cache } = require('../../../helpers'); @@ -8,22 +7,17 @@ const { } = require('../../../cronjob/trailingTradeHelper/configuration'); const { - isActionDisabled + isActionDisabled, + getCacheTrailingTradeSymbols, + getCacheTrailingTradeTotalProfitAndLoss, + getCacheTrailingTradeQuoteEstimates } = require('../../../cronjob/trailingTradeHelper/common'); -const getSymbolFromKey = key => { - const fragments = key.split('-'); - const symbol = fragments[0]; - fragments.shift(); - return { - symbol, - newKey: fragments.join('-') - }; -}; - const handleLatest = async (logger, ws, payload) => { const globalConfiguration = await getConfiguration(logger); - logger.info({ globalConfiguration }, 'Configuration from MongoDB'); + // logger.info({ globalConfiguration }, 'Configuration from MongoDB'); + + const { sortByDesc, sortBy, searchKeyword, page } = payload.data; // If not authenticated and lock list is enabled, then do not send any information. if ( @@ -51,12 +45,29 @@ const handleLatest = async (logger, ws, payload) => { 'trailing-trade-common:', 'trailing-trade-common:*' ); + const cacheTradingView = await cache.hgetall( + 'trailing-trade-tradingview:', + 'trailing-trade-tradingview:*' + ); + + const symbolsPerPage = 12; - const cacheTrailingTradeSymbols = await cache.hgetall( - 'trailing-trade-symbols:', - 'trailing-trade-symbols:*-processed-data' + const symbolsCount = globalConfiguration.symbols.length; + const totalPages = _.ceil(symbolsCount / symbolsPerPage); + + const cacheTrailingTradeSymbols = await getCacheTrailingTradeSymbols( + logger, + sortByDesc, + sortBy, + page, + symbolsPerPage, + searchKeyword ); + // Calculate total profit/loss + const cacheTrailingTradeTotalProfitAndLoss = + await getCacheTrailingTradeTotalProfitAndLoss(logger); + const cacheTrailingTradeClosedTrades = _.map( await cache.hgetall( 'trailing-trade-closed-trades:', @@ -65,22 +76,67 @@ const handleLatest = async (logger, ws, payload) => { stats => JSON.parse(stats) ); + const streamsCount = await cache.hget('trailing-trade-streams', 'count'); + const stats = { - symbols: {} + symbols: await Promise.all( + _.map(cacheTrailingTradeSymbols, async symbol => { + const newSymbol = { ...symbol }; + try { + newSymbol.tradingView = JSON.parse( + cacheTradingView[newSymbol.symbol] + ); + } catch (e) { + _.unset(newSymbol, 'tradingView'); + } + + // Retrieve action disabled + newSymbol.isActionDisabled = await isActionDisabled(newSymbol.symbol); + return newSymbol; + }) + ) }; + const cacheTrailingTradeQuoteEstimates = + await getCacheTrailingTradeQuoteEstimates(logger); + const quoteEstimatesGroupedByBaseAsset = _.groupBy( + cacheTrailingTradeQuoteEstimates, + 'baseAsset' + ); + let common = {}; try { + const accountInfo = JSON.parse(cacheTrailingTradeCommon['account-info']); + accountInfo.balances = accountInfo.balances.map(balance => { + const quoteEstimate = { + quote: null, + estimate: null, + tickSize: null + }; + + if (quoteEstimatesGroupedByBaseAsset[balance.asset]) { + quoteEstimate.quote = + quoteEstimatesGroupedByBaseAsset[balance.asset][0].quoteAsset; + quoteEstimate.estimate = + quoteEstimatesGroupedByBaseAsset[balance.asset][0].estimatedValue; + quoteEstimate.tickSize = + quoteEstimatesGroupedByBaseAsset[balance.asset][0].tickSize; + } + + return { + ...balance, + ...quoteEstimate + }; + }); + common = { version, gitHash: process.env.GIT_HASH || 'unspecified', - accountInfo: JSON.parse(cacheTrailingTradeCommon['account-info']), - exchangeSymbols: JSON.parse(cacheTrailingTradeCommon['exchange-symbols']), + accountInfo, apiInfo: binance.client.getInfo(), closedTradesSetting: JSON.parse( cacheTrailingTradeCommon['closed-trades'] ), - closedTrades: cacheTrailingTradeClosedTrades, orderStats: { numberOfOpenTrades: parseInt( cacheTrailingTradeCommon['number-of-open-trades'], @@ -90,31 +146,18 @@ const handleLatest = async (logger, ws, payload) => { cacheTrailingTradeCommon['number-of-buy-open-orders'], 10 ) - } + }, + closedTrades: cacheTrailingTradeClosedTrades, + totalProfitAndLoss: cacheTrailingTradeTotalProfitAndLoss, + streamsCount, + symbolsCount, + totalPages }; - } catch (e) { - logger.error({ e }, 'Something wrong with trailing-trade-common cache'); + } catch (err) { + logger.error({ err }, 'Something wrong with trailing-trade-common cache'); return; } - _.forIn(cacheTrailingTradeSymbols, (value, key) => { - const { symbol, newKey } = getSymbolFromKey(key); - - if (newKey === 'processed-data') { - stats.symbols[symbol] = JSON.parse(value); - } - }); - - stats.symbols = await Promise.all( - _.map(stats.symbols, async symbol => { - const newSymbol = symbol; - - // Retreive action disabled - newSymbol.isActionDisabled = await isActionDisabled(newSymbol.symbol); - return newSymbol; - }) - ); - logger.info( { account: common.accountInfo, diff --git a/app/frontend/websocket/handlers/manual-trade-all-symbols.js b/app/frontend/websocket/handlers/manual-trade-all-symbols.js index cd913338..8103738b 100644 --- a/app/frontend/websocket/handlers/manual-trade-all-symbols.js +++ b/app/frontend/websocket/handlers/manual-trade-all-symbols.js @@ -7,6 +7,7 @@ const { const { saveOverrideAction } = require('../../../cronjob/trailingTradeHelper/common'); +const { executeTrailingTrade } = require('../../../cronjob'); const handleManualTradeAllSymbols = async (logger, ws, payload) => { logger.info({ payload }, 'Start manual trade all symbols'); @@ -33,7 +34,7 @@ const handleManualTradeAllSymbols = async (logger, ws, payload) => { let currentTime = moment(); if (side === 'buy') { _.forOwn(buy.symbols, (quoteAsset, _quoteSymbol) => { - _.forOwn(quoteAsset.baseAssets, (baseAsset, _baseSymbol) => { + _.forOwn(quoteAsset.baseAssets, async (baseAsset, _baseSymbol) => { const { symbol } = baseAsset; const quoteOrderQty = parseFloat(baseAsset.quoteOrderQty); @@ -54,13 +55,15 @@ const handleManualTradeAllSymbols = async (logger, ws, payload) => { logger.info({ symbolOrder }, `Queueing order for ${symbol}.`); - saveOverrideAction( + await saveOverrideAction( logger, symbol, symbolOrder, `Order for ${symbol} has been queued.` ); + executeTrailingTrade(logger, symbol); + currentTime = moment(currentTime).add( placeManualOrderInterval, 'seconds' @@ -72,7 +75,7 @@ const handleManualTradeAllSymbols = async (logger, ws, payload) => { if (side === 'sell') { _.forOwn(sell.symbols, (quoteAsset, _quoteSymbol) => { - _.forOwn(quoteAsset.baseAssets, (baseAsset, _baseSymbol) => { + _.forOwn(quoteAsset.baseAssets, async (baseAsset, _baseSymbol) => { const { symbol } = baseAsset; const marketQuantity = parseFloat(baseAsset.marketQuantity); @@ -93,13 +96,15 @@ const handleManualTradeAllSymbols = async (logger, ws, payload) => { logger.info({ symbolOrder }, `Queueing order for ${symbol}.`); - saveOverrideAction( + await saveOverrideAction( logger, symbol, symbolOrder, `Order for ${symbol} has been queued.` ); + executeTrailingTrade(logger, symbol); + currentTime = moment(currentTime).add( placeManualOrderInterval, 'seconds' diff --git a/app/frontend/websocket/handlers/manual-trade.js b/app/frontend/websocket/handlers/manual-trade.js index 78a31155..e1217d80 100644 --- a/app/frontend/websocket/handlers/manual-trade.js +++ b/app/frontend/websocket/handlers/manual-trade.js @@ -1,4 +1,5 @@ const moment = require('moment'); +const { executeTrailingTrade } = require('../../../cronjob'); const { saveOverrideAction } = require('../../../cronjob/trailingTradeHelper/common'); @@ -22,6 +23,8 @@ const handleManualTrade = async (logger, ws, payload) => { 'The manual order received by the bot. Wait for placing the order.' ); + executeTrailingTrade(logger, symbol); + ws.send( JSON.stringify({ result: true, diff --git a/app/frontend/websocket/handlers/symbol-delete.js b/app/frontend/websocket/handlers/symbol-delete.js index 3b61a390..7967ccf0 100644 --- a/app/frontend/websocket/handlers/symbol-delete.js +++ b/app/frontend/websocket/handlers/symbol-delete.js @@ -22,6 +22,8 @@ const handleSymbolDelete = async (logger, ws, payload) => { await mongo.deleteOne(logger, 'trailing-trade-symbols', { key }); }); + await mongo.deleteOne(logger, 'trailing-trade-cache', { symbol }); + ws.send(JSON.stringify({ result: true, type: 'symbol-delete-result' })); }; diff --git a/app/frontend/websocket/handlers/symbol-enable-action.js b/app/frontend/websocket/handlers/symbol-enable-action.js index cf1a744a..8bc35268 100644 --- a/app/frontend/websocket/handlers/symbol-enable-action.js +++ b/app/frontend/websocket/handlers/symbol-enable-action.js @@ -1,3 +1,4 @@ +const { executeTrailingTrade } = require('../../../cronjob'); const { deleteDisableAction } = require('../../../cronjob/trailingTradeHelper/common'); @@ -11,6 +12,8 @@ const handleSymbolEnableAction = async (logger, ws, payload) => { await deleteDisableAction(logger, symbol); + executeTrailingTrade(logger, symbol); + ws.send( JSON.stringify({ result: true, type: 'symbol-enable-action-result' }) ); diff --git a/app/frontend/websocket/handlers/symbol-grid-trade-delete.js b/app/frontend/websocket/handlers/symbol-grid-trade-delete.js index da9f430c..a8a07a3a 100644 --- a/app/frontend/websocket/handlers/symbol-grid-trade-delete.js +++ b/app/frontend/websocket/handlers/symbol-grid-trade-delete.js @@ -8,6 +8,7 @@ const { archiveSymbolGridTrade, deleteSymbolGridTrade } = require('../../../cronjob/trailingTradeHelper/configuration'); +const { executeTrailingTrade } = require('../../../cronjob'); const handleSymbolGridTradeDelete = async (logger, ws, payload) => { logger.info({ payload }, 'Start grid trade delete'); @@ -29,7 +30,7 @@ const handleSymbolGridTradeDelete = async (logger, ws, payload) => { } (${moment().format('HH:mm:ss.SSS')}):\n` + `\`\`\`` + ` - Profit: ${archivedGridTrade.profit}\n` + - ` - ProfitPercentage: ${archivedGridTrade.profitPercentage}\n` + + ` - Profit Percentage: ${archivedGridTrade.profitPercentage}\n` + ` - Total Buy Amount: ${archivedGridTrade.totalBuyQuoteQty}\n` + ` - Total Sell Amount: ${archivedGridTrade.totalSellQuoteQty}\n` + `\`\`\`\n` + @@ -40,6 +41,8 @@ const handleSymbolGridTradeDelete = async (logger, ws, payload) => { await deleteSymbolGridTrade(logger, symbol); + executeTrailingTrade(logger, symbol); + ws.send( JSON.stringify({ result: true, type: 'symbol-grid-trade-delete-result' }) ); diff --git a/app/frontend/websocket/handlers/symbol-setting-delete.js b/app/frontend/websocket/handlers/symbol-setting-delete.js index 8872b8a8..288c44e4 100644 --- a/app/frontend/websocket/handlers/symbol-setting-delete.js +++ b/app/frontend/websocket/handlers/symbol-setting-delete.js @@ -1,6 +1,7 @@ const { deleteSymbolConfiguration } = require('../../../cronjob/trailingTradeHelper/configuration'); +const { executeTrailingTrade } = require('../../../cronjob'); const handleSymbolSettingDelete = async (logger, ws, payload) => { logger.info({ payload }, 'Start symbol setting delete'); @@ -11,6 +12,8 @@ const handleSymbolSettingDelete = async (logger, ws, payload) => { await deleteSymbolConfiguration(logger, symbol); + executeTrailingTrade(logger, symbol); + ws.send( JSON.stringify({ result: true, type: 'symbol-setting-delete-result' }) ); diff --git a/app/frontend/websocket/handlers/symbol-setting-update.js b/app/frontend/websocket/handlers/symbol-setting-update.js index 5e9d0c9c..011dc115 100644 --- a/app/frontend/websocket/handlers/symbol-setting-update.js +++ b/app/frontend/websocket/handlers/symbol-setting-update.js @@ -3,6 +3,7 @@ const { getSymbolConfiguration, saveSymbolConfiguration } = require('../../../cronjob/trailingTradeHelper/configuration'); +const { executeTrailingTrade } = require('../../../cronjob'); const handleSymbolSettingUpdate = async (logger, ws, payload) => { logger.info({ payload }, 'Start symbol setting update'); @@ -43,6 +44,8 @@ const handleSymbolSettingUpdate = async (logger, ws, payload) => { await saveSymbolConfiguration(logger, symbol, symbolConfiguration); + executeTrailingTrade(logger, symbol); + ws.send( JSON.stringify({ result: true, diff --git a/app/frontend/websocket/handlers/symbol-trigger-buy.js b/app/frontend/websocket/handlers/symbol-trigger-buy.js index a6714234..cfdc435a 100644 --- a/app/frontend/websocket/handlers/symbol-trigger-buy.js +++ b/app/frontend/websocket/handlers/symbol-trigger-buy.js @@ -2,6 +2,7 @@ const moment = require('moment'); const { saveOverrideAction } = require('../../../cronjob/trailingTradeHelper/common'); +const { executeTrailingTrade } = require('../../../cronjob'); const handleSymbolTriggerBuy = async (logger, ws, payload) => { logger.info({ payload }, 'Start symbol trigger buy'); @@ -24,6 +25,8 @@ const handleSymbolTriggerBuy = async (logger, ws, payload) => { 'The buy order received by the bot. Wait for placing the order.' ); + executeTrailingTrade(logger, symbol); + ws.send(JSON.stringify({ result: true, type: 'symbol-trigger-buy-result' })); }; diff --git a/app/frontend/websocket/handlers/symbol-trigger-sell.js b/app/frontend/websocket/handlers/symbol-trigger-sell.js index 3e9c58de..2a48aecc 100644 --- a/app/frontend/websocket/handlers/symbol-trigger-sell.js +++ b/app/frontend/websocket/handlers/symbol-trigger-sell.js @@ -2,6 +2,7 @@ const moment = require('moment'); const { saveOverrideAction } = require('../../../cronjob/trailingTradeHelper/common'); +const { executeTrailingTrade } = require('../../../cronjob'); const handleSymbolTriggerSell = async (logger, ws, payload) => { logger.info({ payload }, 'Start symbol trigger sell'); @@ -21,6 +22,8 @@ const handleSymbolTriggerSell = async (logger, ws, payload) => { 'The sell order received by the bot. Wait for placing the order.' ); + executeTrailingTrade(logger, symbol); + ws.send(JSON.stringify({ result: true, type: 'symbol-trigger-sell-result' })); }; diff --git a/app/frontend/websocket/handlers/symbol-update-last-buy-price.js b/app/frontend/websocket/handlers/symbol-update-last-buy-price.js index fee5468d..b80a8063 100644 --- a/app/frontend/websocket/handlers/symbol-update-last-buy-price.js +++ b/app/frontend/websocket/handlers/symbol-update-last-buy-price.js @@ -4,6 +4,7 @@ const { getAccountInfo, saveLastBuyPrice } = require('../../../cronjob/trailingTradeHelper/common'); +const { executeTrailingTrade } = require('../../../cronjob'); /** * Delete last buy price @@ -18,6 +19,8 @@ const deleteLastBuyPrice = async (logger, ws, symbol) => { key: `${symbol}-last-buy-price` }); + executeTrailingTrade(logger, symbol); + PubSub.publish('frontend-notification', { type: 'success', title: `The last buy price for ${symbol} has been removed successfully.` @@ -94,6 +97,8 @@ const updateLastBuyPrice = async (logger, ws, symbol, lastBuyPrice) => { quantity: baseAssetTotalBalance }); + executeTrailingTrade(logger, symbol); + PubSub.publish('frontend-notification', { type: 'success', title: `The last buy price for ${symbol} has been configured successfully.` diff --git a/app/helpers/__tests__/binance.test.js b/app/helpers/__tests__/binance.test.js index c01a20ea..28347b46 100644 --- a/app/helpers/__tests__/binance.test.js +++ b/app/helpers/__tests__/binance.test.js @@ -32,7 +32,8 @@ describe('binance', () => { expect(Binance).toHaveBeenCalledWith({ apiKey: 'value-binance.test.apiKey', apiSecret: 'value-binance.test.secretKey', - httpBase: 'https://testnet.binance.vision' + httpBase: 'https://testnet.binance.vision', + wsBase: 'wss://testnet.binance.vision/ws' }); }); diff --git a/app/helpers/binance.js b/app/helpers/binance.js index d4a32dea..fcb38c66 100644 --- a/app/helpers/binance.js +++ b/app/helpers/binance.js @@ -9,6 +9,7 @@ if (config.get('mode') === 'live') { binanceOptions.apiSecret = config.get('binance.live.secretKey'); } else { binanceOptions.httpBase = 'https://testnet.binance.vision'; + binanceOptions.wsBase = 'wss://testnet.binance.vision/ws'; binanceOptions.apiKey = config.get('binance.test.apiKey'); binanceOptions.apiSecret = config.get('binance.test.secretKey'); } diff --git a/app/server-binance.js b/app/server-binance.js index ce1ad21b..ee8083e9 100644 --- a/app/server-binance.js +++ b/app/server-binance.js @@ -1,196 +1,222 @@ const _ = require('lodash'); -const moment = require('moment-timezone'); +// const moment = require('moment-timezone'); const config = require('config'); -const { PubSub, binance, cache, slack } = require('./helpers'); +const moment = require('moment'); +const { PubSub, cache, mongo, slack } = require('./helpers'); -const { getAccountInfo } = require('./cronjob/trailingTradeHelper/common'); const { maskConfig } = require('./cronjob/trailingTradeHelper/util'); - const { getGlobalConfiguration } = require('./cronjob/trailingTradeHelper/configuration'); +const { + lockSymbol, + unlockSymbol, + getAccountInfoFromAPI, + cacheExchangeSymbols, + getAPILimit +} = require('./cronjob/trailingTradeHelper/common'); +const { + getWebsocketATHCandlesClean, + setupATHCandlesWebsocket, + syncATHCandles +} = require('./binance/ath-candles'); +const { + getWebsocketCandlesClean, + setupCandlesWebsocket, + syncCandles +} = require('./binance/candles'); +const { + getWebsocketTickersClean, + refreshTickersClean, + setupTickersWebsocket +} = require('./binance/tickers'); +const { syncOpenOrders, syncDatabaseOrders } = require('./binance/orders'); +const { setupUserWebsocket } = require('./binance/user'); -let websocketCandlesClean; - -let lastReceivedAt = moment(); +let exchangeSymbolsInterval; /** - * Retrieve the list of symbols to watch - * - This includes account's symbol to BTC pairs. + * Setup websocket streams * - * @param {*} _logger - * @param {*} param1 - * @returns + * @param {*} logger + * @param {*} symbols */ -const monitoringSymbols = async ( - _logger, - { globalConfiguration, accountInfo } -) => { - const { symbols: globalSymbols } = globalConfiguration; - // To cut the reference for the global configuration symbols - const symbols = _.cloneDeep(globalSymbols); - - const cachedExchangeSymbols = - JSON.parse(await cache.hget('trailing-trade-common', 'exchange-symbols')) || - {}; - - return accountInfo.balances.reduce((acc, b) => { - const symbol = `${b.asset}BTC`; - // Make sure the symbol existing in Binance. Otherwise, just ignore. - if ( - cachedExchangeSymbols[symbol] !== undefined && - acc.includes(symbol) === false - ) { - acc.push(symbol); - } - return acc; - }, symbols); +const setupWebsockets = async (logger, symbols) => { + await Promise.all([ + setupUserWebsocket(logger), + setupCandlesWebsocket(logger, symbols), + setupATHCandlesWebsocket(logger, symbols), + setupTickersWebsocket(logger, symbols) + ]); + + await cache.hset( + 'trailing-trade-streams', + 'count', + 1 + + _.size(getWebsocketTickersClean()) + + _.size(getWebsocketCandlesClean()) + + _.size(getWebsocketATHCandlesClean()) + ); }; /** - * Setup web socket for retrieving candles + * Setup exchange symbols and store them in cache * * @param {*} logger */ -const setWebSocketCandles = async logger => { - logger.info('Set websocket for candles'); - - // Get configuration - const globalConfiguration = await getGlobalConfiguration(logger); - - // Retrieve account info from cache - const accountInfo = await getAccountInfo(logger); - - // Retrieve list of monitoring symbols including assets BTC pairs - const symbols = await monitoringSymbols(logger, { - globalConfiguration, - accountInfo - }); +const setupExchangeSymbols = async logger => { + if (exchangeSymbolsInterval) { + clearInterval(exchangeSymbolsInterval); + } - logger.info({ symbols }, 'Retrieved symbols'); + // Retrieve exchange symbols and cache it + await cacheExchangeSymbols(logger); - if (websocketCandlesClean) { - logger.info('Existing opened socket for candles found, clean first'); - websocketCandlesClean(); - } - websocketCandlesClean = binance.client.ws.candles(symbols, '1m', candle => { - logger.info({ candle }, 'Received new candle'); + exchangeSymbolsInterval = setInterval(async () => { + // Retrieve exchange symbols and cache it + await cacheExchangeSymbols(logger); + }, 60 * 1000); +}; - // Record last received date/time - lastReceivedAt = moment(); +const refreshCandles = async logger => { + refreshTickersClean(logger); - // Save latest candle for the symbol - cache.hset( - 'trailing-trade-symbols', - `${candle.symbol}-latest-candle`, - JSON.stringify(candle) - ); - }); + // empty all candles before restarting the bot + await mongo.deleteAll(logger, 'trailing-trade-candles', {}); + await mongo.deleteAll(logger, 'trailing-trade-ath-candles', {}); }; /** - * Setup retrieving latest candle from live server via Web Socket + * Setup web socket for retrieving candles * * @param {*} logger */ -const setupLive = async logger => { - PubSub.subscribe('reset-binance-websocket', async (message, data) => { - logger.info(`Message: ${message}, Data: ${data}`); - await setWebSocketCandles(logger); - }); +const syncAll = async logger => { + logger.info('Start syncing the bot...'); - await setWebSocketCandles(logger); -}; + // reset candles and streams before restart to remove any unused symbol + await refreshCandles(logger); -const loopToCheckLastReceivedAt = async logger => { - const currentTime = moment(); + // Retrieve account info from API + await getAccountInfoFromAPI(logger); - // If last received candle time is more than a mintues, then it means something went wrong. Reconnect websocket. - if (lastReceivedAt.diff(currentTime) / 1000 < -60) { - logger.warn( - 'Binance candle is not received in last mintues. Reconfigure websocket' - ); + // Get configuration + const globalConfiguration = await getGlobalConfiguration(logger); - if (config.get('featureToggle.notifyDebug')) { - slack.sendMessage( - `Binance Websocket (${moment().format( - 'HH:mm:ss.SSS' - )}): The bot didn't receive new candle from Binance Websocket since ${lastReceivedAt.fromNow()}.` + - ` Reset Websocket connection.` - ); - } + // Retrieve list of monitoring symbols + const { symbols } = globalConfiguration; - await setupLive(logger); - } + logger.info({ symbols }, 'Retrieved symbols'); + + // Lock all symbols for 5 minutes to ensure nothing will be executed unless all data retrieved + await Promise.all(symbols.map(symbol => lockSymbol(logger, symbol, 300))); - setTimeout(() => loopToCheckLastReceivedAt(logger), 1000); + await setupExchangeSymbols(logger); + await setupWebsockets(logger, symbols); + + await syncCandles(logger, symbols); + await syncATHCandles(logger, symbols); + await syncOpenOrders(logger, symbols); + await syncDatabaseOrders(logger); + + // Unlock all symbols when all data has been retrieved + await Promise.all(symbols.map(symbol => unlockSymbol(logger, symbol))); }; /** - * Setup retrieving latest candle from test server via API + * Setup retrieving latest candle from live server via Web Socket * * @param {*} logger */ -const setupTest = async logger => { - // Get configuration - const globalConfiguration = await getGlobalConfiguration(logger); +const setupBinance = async logger => { + PubSub.subscribe('reset-all-websockets', async (message, data) => { + logger.info(`Message: ${message}, Data: ${data}`); - // Retrieve account info from cache - const accountInfo = await getAccountInfo(logger); + PubSub.publish('frontend-notification', { + type: 'info', + title: 'Restarting bot...' + }); - // Retrieve list of monitoring symbols including assets BTC pairs - const symbols = await monitoringSymbols(logger, { - globalConfiguration, - accountInfo + await syncAll(logger); }); + PubSub.subscribe('reset-symbol-websockets', async (message, data) => { + logger.info(`Message: ${message}, Data: ${data}`); - logger.info({ symbols }, 'Retrieved symbols'); + const symbol = message; - const currentPrices = await binance.client.prices(); + PubSub.publish('frontend-notification', { + type: 'info', + title: `Restarting ${symbol} websockets...` + }); - _.forEach(currentPrices, (currentPrice, currentSymbol) => { - if (symbols.includes(currentSymbol)) { - logger.info({ currentSymbol, currentPrice }, 'Received new price'); + // Get configuration + const globalConfiguration = await getGlobalConfiguration(logger); - cache.hset( - 'trailing-trade-symbols', - `${currentSymbol}-latest-candle`, - JSON.stringify({ - eventType: 'kline', - symbol: currentSymbol, - close: currentPrice - }) - ); - } + // Retrieve list of monitoring symbols + const { symbols } = globalConfiguration; + + // Candles & ATH candles should receive all monitoring symbols to create their connection from scratch + // because they are grouped by symbols intervals and not by their names + await Promise.all([ + setupCandlesWebsocket(logger, symbols), + setupATHCandlesWebsocket(logger, symbols), + setupTickersWebsocket(logger, [symbol]) + ]); + + await syncCandles(logger, [symbol]); + await syncATHCandles(logger, [symbol]); }); - // It's impossible to test async function in the setTimeout. - /* istanbul ignore next */ - setTimeout(() => setupTest(logger), 1000); + await syncAll(logger); }; /** * Configure Binance Web Socket * - * Note that Binance Test Server Web Socket is not providing test server's candles. - * To avoid the issue with the test server, when the mode is test, it will use API call to retrieve current prices. - * * @param {*} serverLogger */ const runBinance = async serverLogger => { const logger = serverLogger.child({ server: 'binance' }); - const mode = config.get('mode'); logger.info( { config: maskConfig(config) }, `Binance ${config.get('mode')} started on` ); - if (mode === 'live') { - await setupLive(logger); - await loopToCheckLastReceivedAt(logger); - } else { - await setupTest(logger); + try { + await setupBinance(logger); + } catch (err) { + // For the redlock fail + if (err.message.includes('redlock')) { + // Simply ignore + return; + } + + logger.error( + { err, errorCode: err.code, debug: true }, + `⚠ Setup binance failed.` + ); + + if ( + err.code === -1001 || + err.code === -1021 || // Timestamp for this request is outside the recvWindow + err.code === 'ECONNRESET' || + err.code === 'ECONNREFUSED' + ) { + // Let's silent for internal server error or assumed temporary errors + } else { + slack.sendMessage( + `Setup binance failed (${moment().format('HH:mm:ss.SSS')})\n` + + `Code: ${err.code}\n` + + `Message:\`\`\`${err.message}\`\`\`\n` + + `${ + config.get('featureToggle.notifyDebug') + ? `Stack:\`\`\`${err.stack}\`\`\`\n` + : '' + }` + + `- Current API Usage: ${getAPILimit(logger)}` + ); + } } }; diff --git a/app/server-cronjob.js b/app/server-cronjob.js index fc39f92a..5c3e019a 100644 --- a/app/server-cronjob.js +++ b/app/server-cronjob.js @@ -3,11 +3,7 @@ const config = require('config'); const { CronJob } = require('cron'); const { maskConfig } = require('./cronjob/trailingTradeHelper/util'); -const { - executeAlive, - executeTrailingTrade, - executeTrailingTradeIndicator -} = require('./cronjob'); +const { executeAlive, executeTrailingTradeIndicator } = require('./cronjob'); const fulfillWithTimeLimit = async (logger, timeLimit, task, failureValue) => { let timeout; @@ -44,7 +40,7 @@ const runCronjob = async serverLogger => { // Execute jobs [ { jobName: 'alive', executeJob: executeAlive }, - { jobName: 'trailingTrade', executeJob: executeTrailingTrade }, + // { jobName: 'trailingTrade', executeJob: executeTrailingTrade }, { jobName: 'trailingTradeIndicator', executeJob: executeTrailingTradeIndicator diff --git a/migrations/1654430019999-create-candles-index.js b/migrations/1654430019999-create-candles-index.js new file mode 100644 index 00000000..84bb4803 --- /dev/null +++ b/migrations/1654430019999-create-candles-index.js @@ -0,0 +1,36 @@ +const path = require('path'); +const { logger: rootLogger, mongo } = require('../app/helpers'); + +module.exports.up = async () => { + const logger = rootLogger.child({ + gitHash: process.env.GIT_HASH || 'unspecified', + migration: path.basename(__filename) + }); + + const database = await mongo.connect(logger); + + logger.info('Start migration'); + + await Promise.all( + ['trailing-trade-candles', 'trailing-trade-ath-candles'].map( + async collectionName => { + const collection = database.collection(collectionName); + + const result = await collection.createIndex( + { key: 1, time: -1, interval: 1 }, + { name: `${collectionName}-idx`, unique: true } + ); + logger.info( + { collectionName, result }, + 'Created collection index for key fields' + ); + } + ) + ); + + logger.info('Finish migration'); +}; + +module.exports.down = next => { + next(); +}; diff --git a/migrations/1655317029674-create-cache-index.js b/migrations/1655317029674-create-cache-index.js new file mode 100644 index 00000000..9a89c142 --- /dev/null +++ b/migrations/1655317029674-create-cache-index.js @@ -0,0 +1,32 @@ +const path = require('path'); +const { logger: rootLogger, mongo } = require('../app/helpers'); + +module.exports.up = async () => { + const logger = rootLogger.child({ + gitHash: process.env.GIT_HASH || 'unspecified', + migration: path.basename(__filename) + }); + + const database = await mongo.connect(logger); + + logger.info('Start migration'); + + const collectionName = 'trailing-trade-cache'; + + const collection = database.collection(collectionName); + + const result = await collection.createIndex( + { symbol: 1 }, + { name: `${collectionName}-idx`, unique: true } + ); + logger.info( + { collectionName, result }, + 'Created collection index for key fields' + ); + + logger.info('Finish migration'); +}; + +module.exports.down = next => { + next(); +}; diff --git a/public/css/App.css b/public/css/App.css index 5a72e252..b3574a12 100644 --- a/public/css/App.css +++ b/public/css/App.css @@ -183,10 +183,9 @@ input[type='number'] { } .coin-info-label-with-icon button { - margin: 0; padding: 0; line-height: 15px; - margin-left: 5px; + margin: 0 0 0 5px; } .coin-info-label-with-icon i { color: #fff; @@ -199,10 +198,9 @@ input[type='number'] { } .coin-info-column-price .coin-info-value-with-icon button { - margin: 0; padding: 0; line-height: 15px; - margin-left: 5px; + margin: 0 0 0 5px; } .coin-info-column-price .coin-info-value-with-icon i { color: #fff; @@ -230,33 +228,13 @@ input[type='number'] { margin-bottom: 10px; } -.account-wrapper-assets { - display: flex; - flex-flow: row wrap; - justify-content: flex-start; -} - -.account-wrapper-asset { - width: 20%; +.account-balance-assets-wrapper { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-gap: 0.5rem; } -@media (max-width: 1200px) { - .account-wrapper-asset { - width: 25%; - } -} - -@media (max-width: 800px) { - .account-wrapper-asset { - width: 33.33%; - } -} - -@media (max-width: 600px) { - .account-wrapper-asset { - width: 50%; - } -} +.account-wrapper-assets {} .account-wrapper-body { -webkit-border-radius: 10px; @@ -284,10 +262,6 @@ input[type='number'] { user-select: none; background-color: transparent; border: 1px solid transparent; - padding: 0.375rem 0.75rem; - font-size: 1rem; - line-height: 1.5; - border-radius: 0.25rem; transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; @@ -424,7 +398,7 @@ input[type='number'] { padding: 5px 0 5px 0; } -.coin-info-sub-open-order-wrapper:first .coin-info-column-title { +.coin-info-sub-open-order-wrapper:first-child .coin-info-column-title { border-top: none; margin-top: 0; padding-top: 0; @@ -554,7 +528,7 @@ input[type='number'] { } .btn-manual { - margin: 2px 0px 0px; + margin: 2px 0 0; appearance: none; user-select: none; cursor: pointer; @@ -963,5 +937,36 @@ input[type='number'] { */ .tradingview-widget-container iframe { - overflow-y: none; + overflow-y: hidden; +} + +/** + Start: Pagination +*/ +.pagination { + justify-content: center; +} + +.pagination .page-link { + color: #809fff; + background-color: #343a40; + border: 1px solid #2d3237; } + +.pagination .page-item.active .page-link { + background-color: #6f42c1; + border-color: #6f42c1; +} + +.pagination .page-link:hover { + color: #fff; + background-color: rgba(110, 66, 193, 0.855); + border-color: rgba(110, 66, 193, 0.855); +} +.pagination .page-item.disabled .page-link { + background-color: #272b38; + border-color: #343a40; +} +/** + End: Pagination +*/ \ No newline at end of file diff --git a/public/js/AccountWrapper.js b/public/js/AccountWrapper.js index f159952c..833ab5fa 100644 --- a/public/js/AccountWrapper.js +++ b/public/js/AccountWrapper.js @@ -3,33 +3,14 @@ /* eslint-disable no-undef */ class AccountWrapper extends React.Component { render() { - const { - accountInfo, - dustTransfer, - sendWebSocket, - isAuthenticated, - quoteEstimates - } = this.props; + const { accountInfo, dustTransfer, sendWebSocket, isAuthenticated } = + this.props; const assets = accountInfo.balances.map((balance, index) => { - let quoteEstimate = quoteEstimates.filter( - elem => elem.baseAsset === balance.asset - ); - - if (quoteEstimate.length == 1) { - quoteEstimate = { - quote: quoteEstimate[0]['quoteAsset'], - estimate: quoteEstimate[0]['estimatedValue'] - }; - } else { - quoteEstimate = null; - } - return ( + balance={balance}> ); }); @@ -49,10 +30,10 @@ class AccountWrapper extends React.Component { -
+
{assets}
-
+
+
-
{balance.asset}
-
- Total: - +
+ {(parseFloat(balance.free) + parseFloat(balance.locked)).toFixed( 5 - )} + )}{' '} + {balance.asset} + {balance.quote !== null ? ( + + {parseFloat(balance.estimate).toFixed(5)} {balance.quote} + + ) : ( +
+ placeholder +
+ )}
- Free: + + Free + {parseFloat(balance.free).toFixed(5)}
-
- Locked: +
0 ? 'text-warning' : '' + }`}> + + Locked + {parseFloat(balance.locked).toFixed(5)}
- {quoteEstimate !== null ? ( -
- - In {quoteEstimate.quote}: - - - {parseFloat(quoteEstimate.estimate).toFixed(5)} - -
- ) : ( -
- placeholder -
- )}
); diff --git a/public/js/App.js b/public/js/App.js index 2ae20ef1..3ba7c0b0 100644 --- a/public/js/App.js +++ b/public/js/App.js @@ -22,26 +22,51 @@ class App extends React.Component { publicURL: '', dustTransfer: {}, availableSortOptions: [ - { sortBy: 'default', label: 'Default' }, - { sortBy: 'buy-difference-asc', label: 'Buy - Difference (asc)' }, - { sortBy: 'buy-difference-desc', label: 'Buy - Difference (desc)' }, - { sortBy: 'sell-profit-asc', label: 'Sell - Profit (asc)' }, - { sortBy: 'sell-profit-desc', label: 'Sell - Profit (desc)' }, - { sortBy: 'alpha-asc', label: 'Alphabetical (asc)' }, - { sortBy: 'alpha-desc', label: 'Alphabetical (desc)' } + { sortBy: 'default', sortByDesc: false, label: 'Default' }, + { + sortBy: 'buy-difference', + sortByDesc: false, + label: 'Buy - Difference (asc)' + }, + { + sortBy: 'buy-difference', + sortByDesc: true, + label: 'Buy - Difference (desc)' + }, + { + sortBy: 'sell-profit', + sortByDesc: false, + label: 'Sell - Profit (asc)' + }, + { + sortBy: 'sell-profit', + sortByDesc: true, + label: 'Sell - Profit (desc)' + }, + { sortBy: 'alpha', sortByDesc: false, label: 'Alphabetical (asc)' }, + { sortBy: 'alpha', sortByDesc: true, label: 'Alphabetical (desc)' } ], - selectedSortOption: 'default', + selectedSortOption: { + sortBy: 'default', + sortByDesc: false + }, searchKeyword: '', isLoaded: false, isAuthenticated: false, botOptions: {}, - authToken: localStorage.getItem('authToken') || '' + authToken: localStorage.getItem('authToken') || '', + totalProfitAndLoss: {}, + streamsCount: 0, + symbolsCount: 0, + page: 1, + totalPages: 1 }; this.requestLatest = this.requestLatest.bind(this); this.connectWebSocket = this.connectWebSocket.bind(this); this.sendWebSocket = this.sendWebSocket.bind(this); this.setSortOption = this.setSortOption.bind(this); this.setSearchKeyword = this.setSearchKeyword.bind(this); + this.setPage = this.setPage.bind(this); this.toast = this.toast.bind(this); @@ -76,7 +101,12 @@ class App extends React.Component { } requestLatest() { - this.sendWebSocket('latest'); + this.sendWebSocket('latest', { + page: this.state.page, + searchKeyword: this.state.searchKeyword, + sortBy: this.state.selectedSortOption.sortBy, + sortByDesc: this.state.selectedSortOption.sortByDesc + }); } toast({ type, title }) { @@ -133,16 +163,20 @@ class App extends React.Component { {} ), closedTrades: _.get(response, ['common', 'closedTrades'], []), - symbols: sortingSymbols(_.get(response, ['stats', 'symbols'], []), { - selectedSortOption: self.state.selectedSortOption, - searchKeyword: self.state.searchKeyword - }), + symbols: _.get(response, ['stats', 'symbols'], []), packageVersion: _.get(response, ['common', 'version'], ''), gitHash: _.get(response, ['common', 'gitHash'], ''), - exchangeSymbols: _.get(response, ['common', 'exchangeSymbols'], []), accountInfo: _.get(response, ['common', 'accountInfo'], {}), publicURL: _.get(response, ['common', 'publicURL'], ''), - apiInfo: _.get(response, ['common', 'apiInfo'], {}) + apiInfo: _.get(response, ['common', 'apiInfo'], {}), + totalProfitAndLoss: _.get( + response, + ['common', 'totalProfitAndLoss'], + '' + ), + streamsCount: _.get(response, ['common', 'streamsCount'], 0), + symbolsCount: _.get(response, ['common', 'symbolsCount'], 0), + totalPages: _.get(response, ['common', 'totalPages'], 1) }); } @@ -158,6 +192,12 @@ class App extends React.Component { dustTransfer: response.dustTransfer }); } + + if (response.type === 'exchange-symbols-get-result') { + self.setState({ + exchangeSymbols: response.exchangeSymbols + }); + } }; instance.onclose = () => { @@ -198,13 +238,31 @@ class App extends React.Component { setSearchKeyword(searchKeyword) { this.setState({ - searchKeyword + searchKeyword, + page: 1 + }); + } + + setPage(newPage) { + this.setState({ + page: newPage }); } componentDidMount() { - const selectedSortOption = - localStorage.getItem('selectedSortOption') || 'default'; + let selectedSortOption = { + sortBy: 'default', + sortByDesc: false + }; + + try { + selectedSortOption = JSON.parse( + localStorage.getItem('selectedSortOption') + ) || { + sortBy: 'default', + sortByDesc: false + }; + } catch (e) {} this.setState({ selectedSortOption @@ -233,13 +291,18 @@ class App extends React.Component { closedTrades, publicURL, apiInfo, + streamsCount, + symbolsCount, dustTransfer, availableSortOptions, selectedSortOption, searchKeyword, isAuthenticated, botOptions, - isLoaded + isLoaded, + totalProfitAndLoss, + page, + totalPages } = this.state; if (isLoaded === false) { @@ -270,14 +333,60 @@ class App extends React.Component { ); }); - const symbolEstimates = symbols.map(symbol => { - return { - baseAsset: symbol.symbolInfo.baseAsset, - quoteAsset: symbol.symbolInfo.quoteAsset, - estimatedValue: symbol.baseAssetBalance.estimatedValue, - tickSize: symbol.symbolInfo.filterPrice.tickSize - }; + const paginationItems = []; + + paginationItems.push( + this.setPage(1)} + /> + ); + paginationItems.push( + this.setPage(page - 1)} + disabled={page === 1 || totalPages === 1} + /> + ); + [...Array(3).keys()].forEach(index => { + if (page === 1 && index === 0) { + paginationItems.push( + this.setPage(page)}> + {page} + + ); + } else { + const pageNum = page === 1 ? page + index : page + index - 1; + paginationItems.push( + totalPages} + key={`pagination-item-${index}`} + onClick={() => this.setPage(pageNum)}> + {pageNum} + + ); + } }); + paginationItems.push( + this.setPage(page + 1)} + disabled={page === totalPages || page >= totalPages} + /> + ); + const lastPage = totalPages; + paginationItems.push( + = totalPages} + onClick={() => this.setPage(lastPage)} + /> + ); return ( @@ -301,7 +410,6 @@ class App extends React.Component { accountInfo={accountInfo} dustTransfer={dustTransfer} sendWebSocket={this.sendWebSocket} - quoteEstimates={symbolEstimates} />
{coinWrappers}
+ {paginationItems}
- +
) : ( diff --git a/public/js/CoinWrapperAction.js b/public/js/CoinWrapperAction.js index 929ae2e8..590ecbc2 100644 --- a/public/js/CoinWrapperAction.js +++ b/public/js/CoinWrapperAction.js @@ -75,13 +75,16 @@ class CoinWrapperAction extends React.Component { ); } + const updatedAt = moment.utc(buy.updatedAt, 'YYYY-MM-DDTHH:mm:ss.SSSSSS'); + const currentTime = moment.utc(); + return (
Action -{' '} - {moment(buy.updatedAt).format('HH:mm:ss')} + {updatedAt.format('HH:mm:ss')} {isLocked === true ? : ''} {isActionDisabled.isDisabled === true ? ( @@ -89,6 +92,33 @@ class CoinWrapperAction extends React.Component { ) : ( '' )} + {updatedAt.isBefore(currentTime, 'minute') ? ( + + + The bot didn't receive the price change for over a min. It + means the price hasn't changed in Binance. It will be + updated when the bot receives a new price change. +
+
+ Last updated: {updatedAt.fromNow()} +
+ + }> + +
+ ) : ( + '' + )}
diff --git a/public/js/FilterIcon.js b/public/js/FilterIcon.js index bcacee75..c656587e 100644 --- a/public/js/FilterIcon.js +++ b/public/js/FilterIcon.js @@ -11,7 +11,10 @@ class FilterIcon extends React.Component { this.state = { showFilterModal: false, - selectedSortOption: 'default', + selectedSortOption: { + sortBy: 'default', + sortByDesc: false + }, searchKeyword: '' }; @@ -70,7 +73,7 @@ class FilterIcon extends React.Component { }); this.props.setSortOption(newSortOption); // Save to local storage - localStorage.setItem('selectedSortOption', newSortOption); + localStorage.setItem('selectedSortOption', JSON.stringify(newSortOption)); } setSearchKeyword(event) { @@ -101,11 +104,14 @@ class FilterIcon extends React.Component {
diff --git a/public/js/ProfitLossWrapper.js b/public/js/ProfitLossWrapper.js index 7606fa20..519293dd 100644 --- a/public/js/ProfitLossWrapper.js +++ b/public/js/ProfitLossWrapper.js @@ -7,7 +7,6 @@ class ProfitLossWrapper extends React.Component { this.state = { canUpdate: true, symbols: {}, - totalPnL: {}, closedTradesLoading: false, closedTradesSetting: {}, selectedPeriod: null @@ -28,25 +27,8 @@ class ProfitLossWrapper extends React.Component { ) { const { symbols } = nextProps; - // Calculate total profit/loss - const totalPnL = {}; - _.forEach(symbols, s => { - if (totalPnL[s.quoteAssetBalance.asset] === undefined) { - totalPnL[s.quoteAssetBalance.asset] = { - asset: s.quoteAssetBalance.asset, - amount: 0, - profit: 0 - }; - } - - totalPnL[s.quoteAssetBalance.asset].amount += - parseFloat(s.baseAssetBalance.total) * s.sell.lastBuyPrice; - totalPnL[s.quoteAssetBalance.asset].profit += s.sell.currentProfit; - }); - this.setState({ - symbols, - totalPnL + symbols }); } @@ -112,60 +94,42 @@ class ProfitLossWrapper extends React.Component { } render() { - const { sendWebSocket, isAuthenticated, closedTrades, symbolEstimates } = + const { sendWebSocket, isAuthenticated, closedTrades, totalProfitAndLoss } = this.props; - const { totalPnL, symbols, selectedPeriod, closedTradesLoading } = - this.state; + const { symbols, selectedPeriod, closedTradesLoading } = this.state; - if (_.isEmpty(totalPnL)) { + if (_.isEmpty(totalProfitAndLoss)) { return ''; } - const groupedEstimates = {}; - symbolEstimates.forEach(symbol => { - if (groupedEstimates[symbol.quoteAsset] === undefined) { - groupedEstimates[symbol.quoteAsset] = { - value: 0, - quotePrecision: - parseFloat(symbol.tickSize) === 1 - ? 0 - : symbol.tickSize.indexOf(1) - 1 - }; - } - - groupedEstimates[symbol.quoteAsset].value += symbol.estimatedValue; - }); - - const openTradeWrappers = Object.values(totalPnL).map((pnl, index) => { - if (groupedEstimates[pnl.asset] === undefined) { - return ''; - } - - const percentage = - pnl.amount > 0 ? ((pnl.profit / pnl.amount) * 100).toFixed(2) : 0; - return ( -
-
-
- {pnl.asset} -
-
- {groupedEstimates[pnl.asset].value.toFixed( - groupedEstimates[pnl.asset].quotePrecision - )} + const openTradeWrappers = Object.values(totalProfitAndLoss).map( + (profitAndLoss, index) => { + const percentage = + profitAndLoss.amount > 0 + ? ((profitAndLoss.profit / profitAndLoss.amount) * 100).toFixed(2) + : 0; + return ( +
+
+
+ {profitAndLoss.asset} +
+
+ {profitAndLoss.estimatedBalance.toFixed(5)} +
+
{' '} +
+ {profitAndLoss.profit > 0 ? '+' : ''} + {profitAndLoss.profit.toFixed(5)} +
({percentage}%)
-
{' '} -
- {pnl.profit > 0 ? '+' : ''} - {pnl.profit.toFixed(5)} -
({percentage}%)
-
- ); - }); + ); + } + ); const closedTradeWrappers = Object.values(closedTrades).map( (stat, index) => { @@ -247,7 +211,7 @@ class ProfitLossWrapper extends React.Component {
- {_.isEmpty(totalPnL) ? ( + {_.isEmpty(totalProfitAndLoss) ? (
Loading... diff --git a/public/js/SettingIcon.js b/public/js/SettingIcon.js index 4a2c1304..ec20fecc 100644 --- a/public/js/SettingIcon.js +++ b/public/js/SettingIcon.js @@ -13,11 +13,12 @@ class SettingIcon extends React.Component { this.state = { showSettingModal: false, showConfirmModal: false, - availableSymbols: [], quoteAssets: [], minNotionals: {}, configuration: {}, - validation: {} + rawConfiguration: {}, + validation: {}, + exchangeSymbols: {} }; this.handleModalShow = this.handleModalShow.bind(this); @@ -61,44 +62,59 @@ class SettingIcon extends React.Component { return { quoteAssets, minNotionals, lastBuyPriceRemoveThresholds }; } - componentDidUpdate(nextProps) { - // Only update configuration, when the modal is closed and different. + isConfigChanged(nextProps) { if ( this.state.showSettingModal === false && _.isEmpty(nextProps.configuration) === false && - _.isEqual(nextProps.configuration, this.state.configuration) === false + _.isEqual(nextProps.configuration, this.state.rawConfiguration) === false + ) { + return true; + } + + return false; + } + + isExchangeSymbolsChanged(nextProps) { + if ( + _.isEmpty(nextProps.exchangeSymbols) === false && + _.isEqual(nextProps.exchangeSymbols, this.state.exchangeSymbols) === false ) { + return true; + } + + return false; + } + + componentDidUpdate(nextProps) { + if (this.isExchangeSymbolsChanged(nextProps)) { const { exchangeSymbols, configuration } = nextProps; const { symbols: selectedSymbols } = configuration; - const availableSymbols = _.reduce( + const { quoteAssets, minNotionals } = this.getQuoteAssets( exchangeSymbols, - (acc, symbol) => { - acc.push(symbol.symbol); - return acc; - }, - [] + selectedSymbols, + configuration.buy.lastBuyPriceRemoveThresholds ); + this.setState({ + quoteAssets, + minNotionals, + exchangeSymbols + }); + } + + // Only update configuration, when the modal is closed and different. + if (this.isConfigChanged(nextProps)) { + const { configuration: rawConfiguration } = nextProps; + const configuration = _.cloneDeep(rawConfiguration); + if (configuration.buy.lastBuyPriceRemoveThresholds === undefined) { configuration.buy.lastBuyPriceRemoveThresholds = {}; } - const { quoteAssets, minNotionals, lastBuyPriceRemoveThresholds } = - this.getQuoteAssets( - exchangeSymbols, - selectedSymbols, - configuration.buy.lastBuyPriceRemoveThresholds - ); - - configuration.buy.lastBuyPriceRemoveThresholds = - lastBuyPriceRemoveThresholds; - this.setState({ - availableSymbols, - quoteAssets, - minNotionals, - configuration + configuration, + rawConfiguration }); } } @@ -112,7 +128,15 @@ class SettingIcon extends React.Component { }); } + componentDidMount() { + this.props.sendWebSocket('exchange-symbols-get'); + } + handleModalShow(modal) { + if (modal === 'setting') { + this.props.sendWebSocket('exchange-symbols-get'); + } + this.setState({ [this.modalToStateMap[modal]]: true }); @@ -175,15 +199,9 @@ class SettingIcon extends React.Component { } render() { - const { isAuthenticated } = this.props; + const { isAuthenticated, exchangeSymbols } = this.props; - const { - configuration, - availableSymbols, - quoteAssets, - minNotionals, - validation - } = this.state; + const { configuration, quoteAssets, minNotionals, validation } = this.state; const { symbols: selectedSymbols } = configuration; if (_.isEmpty(configuration) || isAuthenticated === false) { @@ -237,7 +255,6 @@ class SettingIcon extends React.Component { onChange={selected => { // Handle selections... const { configuration } = this.state; - const { exchangeSymbols } = this.props; configuration.symbols = selected; @@ -253,6 +270,7 @@ class SettingIcon extends React.Component { configuration.buy.lastBuyPriceRemoveThresholds = lastBuyPriceRemoveThresholds; + this.setState({ configuration, quoteAssets, @@ -260,7 +278,7 @@ class SettingIcon extends React.Component { }); }} size='sm' - options={availableSymbols} + options={_.keys(exchangeSymbols)} defaultSelected={selectedSymbols} placeholder='Choose symbols to monitor...' /> diff --git a/public/js/SettingIconBotOptions.js b/public/js/SettingIconBotOptions.js index e06c2fbe..b0f6f32d 100644 --- a/public/js/SettingIconBotOptions.js +++ b/public/js/SettingIconBotOptions.js @@ -266,8 +266,8 @@ class SettingIconBotOptions extends React.Component { type='number' placeholder='Enter minutes' required - min='1' - step='1' + min='0.1' + step='0.1' data-state-key='autoTriggerBuy.triggerAfter' value={ botOptions.autoTriggerBuy.triggerAfter diff --git a/public/js/Status.js b/public/js/Status.js index c4c1d6c9..8df6b9e9 100644 --- a/public/js/Status.js +++ b/public/js/Status.js @@ -3,7 +3,7 @@ /* eslint-disable no-undef */ class Status extends React.Component { render() { - const { apiInfo } = this.props; + const { apiInfo, streamsCount, symbolsCount } = this.props; if (!apiInfo) { return ''; @@ -25,12 +25,25 @@ class Status extends React.Component { -
    +
    • Used Weight (1m):{' '} {apiInfo.spot.usedWeight1m} + /1200 +
    • +
    • + Streams Count (Max: 1024):{' '} + + {streamsCount} + +
    • +
    • + Symbols:{' '} + + {symbolsCount} +
    diff --git a/public/js/SymbolSettingIconBotOptions.js b/public/js/SymbolSettingIconBotOptions.js index 2be42e4e..389e7df2 100644 --- a/public/js/SymbolSettingIconBotOptions.js +++ b/public/js/SymbolSettingIconBotOptions.js @@ -158,8 +158,8 @@ class SymbolSettingIconBotOptions extends React.Component { type='number' placeholder='Enter minutes' required - min='0' - step='1' + min='0.1' + step='0.1' data-state-key='autoTriggerBuy.triggerAfter' value={ botOptions.autoTriggerBuy.triggerAfter