From 5b7b2f8d96687ea7932cf83ef49ef1e0c8485857 Mon Sep 17 00:00:00 2001 From: chrisleekr Date: Tue, 23 Aug 2022 23:27:31 +1000 Subject: [PATCH] fix: archive grid order if all sell orders are executed (#490) --- .../__tests__/remove-last-buy-price.test.js | 1112 +++++++++-------- .../step/remove-last-buy-price.js | 199 ++- 2 files changed, 704 insertions(+), 607 deletions(-) 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 18c7f1b0..13736f9c 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 @@ -6,6 +6,7 @@ describe('remove-last-buy-price.js', () => { let PubSubMock; let slackMock; + let binanceMock; let loggerMock; let mockGetAPILimit; @@ -17,6 +18,7 @@ describe('remove-last-buy-price.js', () => { let mockArchiveSymbolGridTrade; let mockDeleteSymbolGridTrade; + let mockGetSymbolGridTrade; let mockGetGridTradeOrder; @@ -26,14 +28,16 @@ describe('remove-last-buy-price.js', () => { }); beforeEach(async () => { - const { PubSub, slack, logger } = require('../../../../helpers'); + const { PubSub, slack, binance, logger } = require('../../../../helpers'); PubSubMock = PubSub; slackMock = slack; loggerMock = logger; + binanceMock = binance; PubSubMock.publish = jest.fn().mockResolvedValue(true); slackMock.sendMessage = jest.fn().mockResolvedValue(true); + binanceMock.client.cancelOrder = jest.fn().mockResolvedValue(true); mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([]); mockGetAPILimit = jest.fn().mockResolvedValue(10); @@ -52,6 +56,7 @@ describe('remove-last-buy-price.js', () => { totalSellQuoteQty: 0 }); mockDeleteSymbolGridTrade = jest.fn().mockResolvedValue(true); + mockGetSymbolGridTrade = jest.fn().mockResolvedValue({}); mockGetGridTradeOrder = jest.fn().mockResolvedValue({}); }); @@ -69,7 +74,8 @@ describe('remove-last-buy-price.js', () => { jest.mock('../../../trailingTradeHelper/configuration', () => ({ archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -139,7 +145,8 @@ describe('remove-last-buy-price.js', () => { jest.mock('../../../trailingTradeHelper/configuration', () => ({ archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade })); jest.mock('../../../trailingTradeHelper/order', () => ({ @@ -196,7 +203,7 @@ describe('remove-last-buy-price.js', () => { }); }); - describe('when grid trade last buy order exists', () => { + describe('when last buy price is not set', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ isActionDisabled: mockIsActionDisabled, @@ -209,15 +216,11 @@ describe('remove-last-buy-price.js', () => { jest.mock('../../../trailingTradeHelper/configuration', () => ({ archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade })); - mockGetGridTradeOrder = jest.fn().mockImplementation((_logger, key) => { - if (key === 'BTCUPUSDT-grid-trade-last-buy-order') { - return { orderId: 123 }; - } - return null; - }); + mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); jest.mock('../../../trailingTradeHelper/order', () => ({ getGridTradeOrder: mockGetGridTradeOrder @@ -273,8 +276,13 @@ describe('remove-last-buy-price.js', () => { }); }); - describe('when grid trade last sell order exists', () => { + describe('when action is disabled', () => { beforeEach(async () => { + mockIsActionDisabled = jest.fn().mockResolvedValue({ + isDisabled: true, + canRemoveLastBuyPrice: false + }); + jest.mock('../../../trailingTradeHelper/common', () => ({ isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, @@ -286,15 +294,11 @@ describe('remove-last-buy-price.js', () => { jest.mock('../../../trailingTradeHelper/configuration', () => ({ archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade })); - mockGetGridTradeOrder = jest.fn().mockImplementation((_logger, key) => { - if (key === 'BTCUPUSDT-grid-trade-last-sell-order') { - return { orderId: 123 }; - } - return null; - }); + mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); jest.mock('../../../trailingTradeHelper/order', () => ({ getGridTradeOrder: mockGetGridTradeOrder @@ -326,7 +330,7 @@ describe('remove-last-buy-price.js', () => { }, sell: { currentPrice: 200, - lastBuyPrice: null + lastBuyPrice: 190 } }; @@ -350,7 +354,7 @@ describe('remove-last-buy-price.js', () => { }); }); - describe('when last buy price is not set', () => { + describe('when grid trade last sell order exists', () => { beforeEach(async () => { jest.mock('../../../trailingTradeHelper/common', () => ({ isActionDisabled: mockIsActionDisabled, @@ -363,10 +367,16 @@ describe('remove-last-buy-price.js', () => { jest.mock('../../../trailingTradeHelper/configuration', () => ({ archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade })); - mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); + mockGetGridTradeOrder = jest.fn().mockImplementation((_logger, key) => { + if (key === 'BTCUPUSDT-grid-trade-last-sell-order') { + return { orderId: 123 }; + } + return null; + }); jest.mock('../../../trailingTradeHelper/order', () => ({ getGridTradeOrder: mockGetGridTradeOrder @@ -398,7 +408,7 @@ describe('remove-last-buy-price.js', () => { }, sell: { currentPrice: 200, - lastBuyPrice: null + lastBuyPrice: 160 } }; @@ -422,8 +432,14 @@ describe('remove-last-buy-price.js', () => { }); }); - describe('when open orders exist', () => { - beforeEach(async () => { + describe('when sell order is completed', () => { + beforeEach(() => { + mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([ + { + orderId: 123456 + } + ]); + jest.mock('../../../trailingTradeHelper/common', () => ({ isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, @@ -433,10 +449,12 @@ describe('remove-last-buy-price.js', () => { getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); - jest.mock('../../../trailingTradeHelper/configuration', () => ({ - archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade - })); + mockArchiveSymbolGridTrade = jest.fn().mockResolvedValue({ + profit: 10, + profitPercentage: 0.1, + totalBuyQuoteBuy: 100, + totalSellQuoteQty: 110 + }); mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); @@ -444,15 +462,19 @@ describe('remove-last-buy-price.js', () => { getGridTradeOrder: mockGetGridTradeOrder })); - const step = require('../remove-last-buy-price'); - rawData = { action: 'not-determined', isLocked: false, symbol: 'BTCUPUSDT', symbolConfiguration: { - symbols: ['BTCUPUSDT', 'BTCUSDT', 'BNBUSDT'], - buy: { lastBuyPriceRemoveThreshold: 10 } + symbols: ['BTCUSDT', 'BNBUSDT', 'BTCUPUSDT'], + buy: { lastBuyPriceRemoveThreshold: 10 }, + botOptions: { + autoTriggerBuy: { + enabled: true, + triggerAfter: 20 + } + } }, symbolInfo: { filterLotSize: { @@ -465,185 +487,91 @@ describe('remove-last-buy-price.js', () => { }, openOrders: [ { - orderId: 123, - price: 197.8, - quantity: 0.09, - side: 'sell', - stopPrice: 198, - symbol: 'BTCUPUSDT', - timeInForce: 'GTC', - type: 'STOP_LOSS_LIMIT' + orderId: 123456 } ], baseAssetBalance: { free: 0, - locked: 0 + locked: 0.2 }, sell: { currentPrice: 200, - lastBuyPrice: 190 + lastBuyPrice: 160 } }; - - result = await step.execute(loggerMock, rawData); - }); - - it('does not trigger archiveSymbolGridTrade', () => { - expect(mockArchiveSymbolGridTrade).not.toHaveBeenCalled(); }); - it('does not trigger deleteSymbolGridTrade', () => { - expect(mockDeleteSymbolGridTrade).not.toHaveBeenCalled(); - }); + describe('when symbol grid trade is empty', () => { + beforeEach(async () => { + mockGetSymbolGridTrade = jest.fn().mockResolvedValue({}); - it('does not trigger saveOrderStats', () => { - expect(mockSaveOrderStats).not.toHaveBeenCalled(); - }); + jest.mock('../../../trailingTradeHelper/configuration', () => ({ + archiveSymbolGridTrade: mockArchiveSymbolGridTrade, + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade + })); - it('returns expected data', () => { - expect(result).toStrictEqual(rawData); - }); - }); + const step = require('../remove-last-buy-price'); - describe('when action is disabled', () => { - beforeEach(async () => { - mockIsActionDisabled = jest.fn().mockResolvedValue({ - isDisabled: true, - canRemoveLastBuyPrice: false + result = await step.execute(loggerMock, rawData); }); - jest.mock('../../../trailingTradeHelper/common', () => ({ - isActionDisabled: mockIsActionDisabled, - getAPILimit: mockGetAPILimit, - removeLastBuyPrice: mockRemoveLastBuyPrice, - saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction, - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol - })); - - jest.mock('../../../trailingTradeHelper/configuration', () => ({ - archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade - })); - - mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); - - jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder - })); - - const step = require('../remove-last-buy-price'); + it('does not trigger binance.client.cancelOrder', () => { + expect(binanceMock.client.cancelOrder).not.toHaveBeenCalled(); + }); - rawData = { - action: 'not-determined', - isLocked: false, - symbol: 'BTCUPUSDT', - symbolConfiguration: { - symbols: ['BTCUPUSDT', 'BTCUSDT', 'BNBUSDT'], - buy: { lastBuyPriceRemoveThreshold: 10 } - }, - symbolInfo: { - filterLotSize: { - stepSize: '0.01000000', - minQty: '0.01000000' - }, - filterMinNotional: { - minNotional: '10.00000000' - } - }, - openOrders: [], - baseAssetBalance: { - free: 0, - locked: 0 - }, - sell: { - currentPrice: 200, - lastBuyPrice: 190 - } - }; + it('does not trigger removeLastBuyPrice', () => { + expect(mockRemoveLastBuyPrice).not.toHaveBeenCalled(); + }); - result = await step.execute(loggerMock, rawData); - }); + it('does not trigger archiveSymbolGridTrade', () => { + expect(mockArchiveSymbolGridTrade).not.toHaveBeenCalled(); + }); - it('does not trigger archiveSymbolGridTrade', () => { - expect(mockArchiveSymbolGridTrade).not.toHaveBeenCalled(); - }); + it('does not trigger deleteSymbolGridTrade', () => { + expect(mockDeleteSymbolGridTrade).not.toHaveBeenCalled(); + }); - it('does not trigger deleteSymbolGridTrade', () => { - expect(mockDeleteSymbolGridTrade).not.toHaveBeenCalled(); - }); + it('does not trigger saveOverrideAction', () => { + expect(mockSaveOverrideAction).not.toHaveBeenCalled(); + }); - it('does not trigger saveOrderStats', () => { - expect(mockSaveOrderStats).not.toHaveBeenCalled(); - }); + it('does not trigger saveOrderStats', () => { + expect(mockSaveOrderStats).not.toHaveBeenCalled(); + }); - it('returns expected data', () => { - expect(result).toStrictEqual(rawData); + it('returns expected data', () => { + expect(result).toStrictEqual({ + ...rawData + }); + }); }); - }); - describe('when quantity is not enough to sell', () => { - describe('when found open orders at this point', () => { + describe('when sell array of symbol grid trade is empty', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([ - { - orderId: '123123123' - } - ]); - - jest.mock('../../../trailingTradeHelper/common', () => ({ - isActionDisabled: mockIsActionDisabled, - getAPILimit: mockGetAPILimit, - removeLastBuyPrice: mockRemoveLastBuyPrice, - saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction, - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol - })); + mockGetSymbolGridTrade = jest.fn().mockResolvedValue({ + sell: [] + }); jest.mock('../../../trailingTradeHelper/configuration', () => ({ archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade - })); - - mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); - - jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade })); const step = require('../remove-last-buy-price'); - rawData = { - action: 'not-determined', - isLocked: false, - symbol: 'BTCUPUSDT', - symbolConfiguration: { - symbols: ['BTCUPUSDT', 'BTCUSDT', 'BNBUSDT'], - buy: { lastBuyPriceRemoveThreshold: 10 } - }, - symbolInfo: { - filterLotSize: { - stepSize: '0.01000000', - minQty: '0.01000000' - }, - filterMinNotional: { - minNotional: '10.00000000' - } - }, - openOrders: [], - baseAssetBalance: { - free: 0, - locked: 0 - }, - sell: { - currentPrice: 200, - lastBuyPrice: 160 - } - }; - result = await step.execute(loggerMock, rawData); }); + it('does not trigger binance.client.cancelOrder', () => { + expect(binanceMock.client.cancelOrder).not.toHaveBeenCalled(); + }); + + it('does not trigger removeLastBuyPrice', () => { + expect(mockRemoveLastBuyPrice).not.toHaveBeenCalled(); + }); + it('does not trigger archiveSymbolGridTrade', () => { expect(mockArchiveSymbolGridTrade).not.toHaveBeenCalled(); }); @@ -652,190 +580,343 @@ describe('remove-last-buy-price.js', () => { expect(mockDeleteSymbolGridTrade).not.toHaveBeenCalled(); }); + it('does not trigger saveOverrideAction', () => { + expect(mockSaveOverrideAction).not.toHaveBeenCalled(); + }); + it('does not trigger saveOrderStats', () => { expect(mockSaveOrderStats).not.toHaveBeenCalled(); }); it('returns expected data', () => { - expect(result).toStrictEqual(rawData); + expect(result).toStrictEqual({ + ...rawData + }); }); }); - describe('when cannot find open orders', () => { - beforeEach(() => { - jest.mock('../../../trailingTradeHelper/common', () => ({ - isActionDisabled: mockIsActionDisabled, - getAPILimit: mockGetAPILimit, - removeLastBuyPrice: mockRemoveLastBuyPrice, - saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction, - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol + describe('there is no sell order executed', () => { + beforeEach(async () => { + mockGetSymbolGridTrade = jest.fn().mockResolvedValue({ + sell: [ + { + executed: true + }, + { + executed: false + } + ] + }); + + jest.mock('../../../trailingTradeHelper/configuration', () => ({ + archiveSymbolGridTrade: mockArchiveSymbolGridTrade, + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade })); - mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); + const step = require('../remove-last-buy-price'); - jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder - })); + result = await step.execute(loggerMock, rawData); }); - [ - { - symbol: 'ALPHABTC', - archivedSymbolGridTradeResult: { - profit: 10, - profitPercentage: 0.1, - totalBuyQuoteBuy: 100, - totalSellQuoteQty: 110 - }, - rawData: { - action: 'not-determined', - isLocked: false, - symbol: 'ALPHABTC', - symbolConfiguration: { - symbols: ['BTCUSDT', 'BNBUSDT', 'ALPHABTC'], - buy: { lastBuyPriceRemoveThreshold: 0.0001 }, - botOptions: { - autoTriggerBuy: { - enabled: true, - triggerAfter: 20 - } - } - }, - symbolInfo: { - filterLotSize: { - stepSize: '1.00000000', - minQty: '1.00000000' - }, - filterMinNotional: { - minNotional: '0.00010000' - } - }, - openOrders: [], - baseAssetBalance: { - free: 1, - locked: 0 - }, - sell: { - currentPrice: 0.000038, - lastBuyPrice: 0.00003179 - } - } - }, - { + it('does not trigger binance.client.cancelOrder', () => { + expect(binanceMock.client.cancelOrder).not.toHaveBeenCalled(); + }); + + it('does not trigger removeLastBuyPrice', () => { + expect(mockRemoveLastBuyPrice).not.toHaveBeenCalled(); + }); + + it('does not trigger archiveSymbolGridTrade', () => { + expect(mockArchiveSymbolGridTrade).not.toHaveBeenCalled(); + }); + + it('does not trigger deleteSymbolGridTrade', () => { + expect(mockDeleteSymbolGridTrade).not.toHaveBeenCalled(); + }); + + it('does not trigger saveOverrideAction', () => { + expect(mockSaveOverrideAction).not.toHaveBeenCalled(); + }); + + it('does not trigger saveOrderStats', () => { + expect(mockSaveOrderStats).not.toHaveBeenCalled(); + }); + + it('returns expected data', () => { + expect(result).toStrictEqual({ + ...rawData + }); + }); + }); + + describe('there is all sell order executed', () => { + beforeEach(async () => { + mockGetSymbolGridTrade = jest.fn().mockResolvedValue({ + sell: [ + { + executed: true + }, + { + executed: true + } + ] + }); + + jest.mock('../../../trailingTradeHelper/configuration', () => ({ + archiveSymbolGridTrade: mockArchiveSymbolGridTrade, + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade + })); + + const step = require('../remove-last-buy-price'); + + result = await step.execute(loggerMock, rawData); + }); + + it('triggers binance.client.cancelOrder', () => { + expect(binanceMock.client.cancelOrder).toHaveBeenCalledWith({ symbol: 'BTCUPUSDT', - archivedSymbolGridTradeResult: {}, - rawData: { - action: 'not-determined', - isLocked: false, - symbol: 'BTCUPUSDT', - symbolConfiguration: { - symbols: ['BTCUSDT', 'BNBUSDT', 'BTCUPUSDT'], - buy: { lastBuyPriceRemoveThreshold: 10 }, - botOptions: { - autoTriggerBuy: { - enabled: false, - triggerAfter: 20 - } + orderId: 123456 + }); + }); + + it('triggers removeLastBuyPrice', () => { + expect(mockRemoveLastBuyPrice).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); + }); + + it('triggers archiveSymbolGridTrade', () => { + expect(mockArchiveSymbolGridTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); + }); + + it('triggers deleteSymbolGridTrade', () => { + expect(mockDeleteSymbolGridTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); + }); + + it('triggers saveOverrideAction', () => { + expect(mockSaveOverrideAction).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT', + { + action: 'buy', + actionAt: expect.any(String), + triggeredBy: 'auto-trigger', + notify: true, + checkTradingView: true + }, + `The bot queued the action to trigger the grid trade for buying after 20 minutes later.` + ); + }); + + it('triggers saveOrderStats', () => { + expect(mockSaveOrderStats).toHaveBeenCalledWith(loggerMock, [ + 'BTCUSDT', + 'BNBUSDT', + 'BTCUPUSDT' + ]); + }); + + it('returns expected data', () => { + expect(result).toStrictEqual({ + ...rawData, + ...{ + sell: { + currentPrice: 200, + lastBuyPrice: 160, + processMessage: + 'All sell orders are executed. Delete last buy price.', + updatedAt: expect.any(Object) + } + } + }); + }); + }); + }); + + describe('when quantity is not enough to sell', () => { + beforeEach(() => { + jest.mock('../../../trailingTradeHelper/common', () => ({ + isActionDisabled: mockIsActionDisabled, + getAPILimit: mockGetAPILimit, + removeLastBuyPrice: mockRemoveLastBuyPrice, + saveOrderStats: mockSaveOrderStats, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol + })); + + mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); + + jest.mock('../../../trailingTradeHelper/order', () => ({ + getGridTradeOrder: mockGetGridTradeOrder + })); + }); + + [ + { + symbol: 'ALPHABTC', + archivedSymbolGridTradeResult: { + profit: 10, + profitPercentage: 0.1, + totalBuyQuoteBuy: 100, + totalSellQuoteQty: 110 + }, + rawData: { + action: 'not-determined', + isLocked: false, + symbol: 'ALPHABTC', + symbolConfiguration: { + symbols: ['BTCUSDT', 'BNBUSDT', 'ALPHABTC'], + buy: { lastBuyPriceRemoveThreshold: 0.0001 }, + botOptions: { + autoTriggerBuy: { + enabled: true, + triggerAfter: 20 } + } + }, + symbolInfo: { + filterLotSize: { + stepSize: '1.00000000', + minQty: '1.00000000' }, - symbolInfo: { - filterLotSize: { - stepSize: '0.01000000', - minQty: '0.01000000' - }, - filterMinNotional: { - minNotional: '10.00000000' + filterMinNotional: { + minNotional: '0.00010000' + } + }, + openOrders: [], + baseAssetBalance: { + free: 1, + locked: 0 + }, + sell: { + currentPrice: 0.000038, + lastBuyPrice: 0.00003179 + } + } + }, + { + symbol: 'BTCUPUSDT', + archivedSymbolGridTradeResult: {}, + rawData: { + action: 'not-determined', + isLocked: false, + symbol: 'BTCUPUSDT', + symbolConfiguration: { + symbols: ['BTCUSDT', 'BNBUSDT', 'BTCUPUSDT'], + buy: { lastBuyPriceRemoveThreshold: 10 }, + botOptions: { + autoTriggerBuy: { + enabled: false, + triggerAfter: 20 } + } + }, + symbolInfo: { + filterLotSize: { + stepSize: '0.01000000', + minQty: '0.01000000' }, - openOrders: [], - baseAssetBalance: { - free: 0, - locked: 0 - }, - sell: { - currentPrice: 200, - lastBuyPrice: 160 + filterMinNotional: { + minNotional: '10.00000000' } + }, + openOrders: [], + baseAssetBalance: { + free: 0, + locked: 0 + }, + sell: { + currentPrice: 200, + lastBuyPrice: 160 } } - ].forEach(test => { - describe(`${test.symbol}`, () => { - beforeEach(async () => { - mockArchiveSymbolGridTrade = jest - .fn() - .mockResolvedValue(test.archivedSymbolGridTradeResult); - - jest.mock('../../../trailingTradeHelper/configuration', () => ({ - archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade - })); + } + ].forEach(test => { + describe(`${test.symbol}`, () => { + beforeEach(async () => { + mockArchiveSymbolGridTrade = jest + .fn() + .mockResolvedValue(test.archivedSymbolGridTradeResult); - const step = require('../remove-last-buy-price'); + jest.mock('../../../trailingTradeHelper/configuration', () => ({ + archiveSymbolGridTrade: mockArchiveSymbolGridTrade, + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade + })); - result = await step.execute(loggerMock, test.rawData); - }); + const step = require('../remove-last-buy-price'); - it('triggers removeLastBuyPrice', () => { - expect(mockRemoveLastBuyPrice).toHaveBeenCalledWith( - loggerMock, - test.symbol - ); - }); + result = await step.execute(loggerMock, test.rawData); + }); - it('triggers archiveSymbolGridTrade', () => { - expect(mockArchiveSymbolGridTrade).toHaveBeenCalledWith( - loggerMock, - test.symbol - ); - }); + it('triggers removeLastBuyPrice', () => { + expect(mockRemoveLastBuyPrice).toHaveBeenCalledWith( + loggerMock, + test.symbol + ); + }); - it('triggers deleteSymbolGridTrade', () => { - expect(mockDeleteSymbolGridTrade).toHaveBeenCalledWith( - loggerMock, - test.symbol - ); - }); + it('triggers archiveSymbolGridTrade', () => { + expect(mockArchiveSymbolGridTrade).toHaveBeenCalledWith( + loggerMock, + test.symbol + ); + }); - if ( - test.rawData.symbolConfiguration.botOptions.autoTriggerBuy.enabled - ) { - it('triggers saveOverrideAction', () => { - expect(mockSaveOverrideAction).toHaveBeenCalledWith( - loggerMock, - test.symbol, - { - action: 'buy', - actionAt: expect.any(String), - triggeredBy: 'auto-trigger', - notify: true, - checkTradingView: true - }, - `The bot queued the action to trigger the grid trade for buying after` + - ` ${test.rawData.symbolConfiguration.botOptions.autoTriggerBuy.triggerAfter} minutes later.` - ); - }); - } else { - it('does not trigger saveOverrideAction', () => { - expect(mockSaveOverrideAction).not.toHaveBeenCalled(); - }); - } + it('triggers deleteSymbolGridTrade', () => { + expect(mockDeleteSymbolGridTrade).toHaveBeenCalledWith( + loggerMock, + test.symbol + ); + }); - it('triggers saveOrderStats', () => { - expect(mockSaveOrderStats).toHaveBeenCalledWith( + if ( + test.rawData.symbolConfiguration.botOptions.autoTriggerBuy.enabled + ) { + it('triggers saveOverrideAction', () => { + expect(mockSaveOverrideAction).toHaveBeenCalledWith( loggerMock, - test.rawData.symbolConfiguration.symbols + test.symbol, + { + action: 'buy', + actionAt: expect.any(String), + triggeredBy: 'auto-trigger', + notify: true, + checkTradingView: true + }, + `The bot queued the action to trigger the grid trade for buying after` + + ` ${test.rawData.symbolConfiguration.botOptions.autoTriggerBuy.triggerAfter} minutes later.` ); }); + } else { + it('does not trigger saveOverrideAction', () => { + expect(mockSaveOverrideAction).not.toHaveBeenCalled(); + }); + } - it('returns expected data', () => { - expect(result).toMatchObject({ - sell: { - processMessage: - 'Balance is not enough to sell. Delete last buy price.', - updatedAt: expect.any(Object) - } - }); + it('triggers saveOrderStats', () => { + expect(mockSaveOrderStats).toHaveBeenCalledWith( + loggerMock, + test.rawData.symbolConfiguration.symbols + ); + }); + + it('returns expected data', () => { + expect(result).toMatchObject({ + sell: { + processMessage: + 'Balance is not enough to sell. Delete last buy price.', + updatedAt: expect.any(Object) + } }); }); }); @@ -843,14 +924,8 @@ describe('remove-last-buy-price.js', () => { }); describe('when balance is less than minimum notional', () => { - describe('when found open orders at this point', () => { + describe('last buy price remove threshold is same as minimum notional', () => { beforeEach(async () => { - mockGetAndCacheOpenOrdersForSymbol = jest.fn().mockResolvedValue([ - { - orderId: '123123123' - } - ]); - jest.mock('../../../trailingTradeHelper/common', () => ({ isActionDisabled: mockIsActionDisabled, getAPILimit: mockGetAPILimit, @@ -860,9 +935,17 @@ describe('remove-last-buy-price.js', () => { getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol })); + mockArchiveSymbolGridTrade = jest.fn().mockResolvedValue({ + profit: -10, + profitPercentage: -0.1, + totalBuyQuoteBuy: 110, + totalSellQuoteQty: 100 + }); + jest.mock('../../../trailingTradeHelper/configuration', () => ({ archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade })); mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); @@ -879,7 +962,13 @@ describe('remove-last-buy-price.js', () => { symbol: 'BTCUPUSDT', symbolConfiguration: { symbols: ['BTCUSDT', 'BNBUSDT', 'BTCUPUSDT'], - buy: { lastBuyPriceRemoveThreshold: 10 } + buy: { lastBuyPriceRemoveThreshold: 10 }, + botOptions: { + autoTriggerBuy: { + enabled: true, + triggerAfter: 20 + } + } }, symbolInfo: { filterLotSize: { @@ -904,243 +993,157 @@ describe('remove-last-buy-price.js', () => { result = await step.execute(loggerMock, rawData); }); - it('does not trigger archiveSymbolGridTrade', () => { - expect(mockArchiveSymbolGridTrade).not.toHaveBeenCalled(); + it('triggers removeLastBuyPrice', () => { + expect(mockRemoveLastBuyPrice).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); }); - it('does not trigger deleteSymbolGridTrade', () => { - expect(mockDeleteSymbolGridTrade).not.toHaveBeenCalled(); + it('triggers archiveSymbolGridTrade', () => { + expect(mockArchiveSymbolGridTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); }); - it('does not trigger saveOrderStats', () => { - expect(mockSaveOrderStats).not.toHaveBeenCalled(); + it('triggers deleteSymbolGridTrade', () => { + expect(mockDeleteSymbolGridTrade).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT' + ); }); - it('returns expected data', () => { - expect(result).toStrictEqual(rawData); + it('triggers saveOverrideAction', () => { + expect(mockSaveOverrideAction).toHaveBeenCalledWith( + loggerMock, + 'BTCUPUSDT', + { + action: 'buy', + actionAt: expect.any(String), + triggeredBy: 'auto-trigger', + notify: true, + checkTradingView: true + }, + `The bot queued the action to trigger the grid trade for buying after 20 minutes later.` + ); }); - }); - describe('when cannot find open orders', () => { - describe('last buy price remove threshold is same as minimum notional', () => { - beforeEach(async () => { - jest.mock('../../../trailingTradeHelper/common', () => ({ - isActionDisabled: mockIsActionDisabled, - getAPILimit: mockGetAPILimit, - removeLastBuyPrice: mockRemoveLastBuyPrice, - saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction, - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol - })); - - mockArchiveSymbolGridTrade = jest.fn().mockResolvedValue({ - profit: -10, - profitPercentage: -0.1, - totalBuyQuoteBuy: 110, - totalSellQuoteQty: 100 - }); - - jest.mock('../../../trailingTradeHelper/configuration', () => ({ - archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade - })); - - mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); - - jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder - })); - - const step = require('../remove-last-buy-price'); + it('triggers saveOrderStats', () => { + expect(mockSaveOrderStats).toHaveBeenCalledWith(loggerMock, [ + 'BTCUSDT', + 'BNBUSDT', + 'BTCUPUSDT' + ]); + }); - rawData = { - action: 'not-determined', - isLocked: false, - symbol: 'BTCUPUSDT', - symbolConfiguration: { - symbols: ['BTCUSDT', 'BNBUSDT', 'BTCUPUSDT'], - buy: { lastBuyPriceRemoveThreshold: 10 }, - botOptions: { - autoTriggerBuy: { - enabled: true, - triggerAfter: 20 - } - } - }, - symbolInfo: { - filterLotSize: { - stepSize: '0.01000000', - minQty: '0.01000000' - }, - filterMinNotional: { - minNotional: '10.00000000' - } - }, - openOrders: [], - baseAssetBalance: { - free: 0, - locked: 0.04 - }, + it('returns expected data', () => { + expect(result).toStrictEqual({ + ...rawData, + ...{ sell: { currentPrice: 200, - lastBuyPrice: 160 - } - }; - - result = await step.execute(loggerMock, rawData); - }); - - it('triggers removeLastBuyPrice', () => { - expect(mockRemoveLastBuyPrice).toHaveBeenCalledWith( - loggerMock, - 'BTCUPUSDT' - ); - }); - - it('triggers archiveSymbolGridTrade', () => { - expect(mockArchiveSymbolGridTrade).toHaveBeenCalledWith( - loggerMock, - 'BTCUPUSDT' - ); - }); - - it('triggers deleteSymbolGridTrade', () => { - expect(mockDeleteSymbolGridTrade).toHaveBeenCalledWith( - loggerMock, - 'BTCUPUSDT' - ); - }); - - it('triggers saveOverrideAction', () => { - expect(mockSaveOverrideAction).toHaveBeenCalledWith( - loggerMock, - 'BTCUPUSDT', - { - action: 'buy', - actionAt: expect.any(String), - triggeredBy: 'auto-trigger', - notify: true, - checkTradingView: true - }, - `The bot queued the action to trigger the grid trade for buying after 20 minutes later.` - ); - }); - - it('triggers saveOrderStats', () => { - expect(mockSaveOrderStats).toHaveBeenCalledWith(loggerMock, [ - 'BTCUSDT', - 'BNBUSDT', - 'BTCUPUSDT' - ]); - }); - - it('returns expected data', () => { - expect(result).toStrictEqual({ - ...rawData, - ...{ - sell: { - currentPrice: 200, - lastBuyPrice: 160, - processMessage: - 'Balance is less than the last buy price remove threshold. Delete last buy price.', - updatedAt: expect.any(Object) - } + lastBuyPrice: 160, + processMessage: + 'Balance is less than the last buy price remove threshold. Delete last buy price.', + updatedAt: expect.any(Object) } - }); + } }); }); + }); - describe('last buy price remove threshold is less than minimum notional', () => { - beforeEach(async () => { - jest.mock('../../../trailingTradeHelper/common', () => ({ - isActionDisabled: mockIsActionDisabled, - getAPILimit: mockGetAPILimit, - removeLastBuyPrice: mockRemoveLastBuyPrice, - saveOrderStats: mockSaveOrderStats, - saveOverrideAction: mockSaveOverrideAction, - getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol - })); + describe('last buy price remove threshold is less than minimum notional', () => { + beforeEach(async () => { + jest.mock('../../../trailingTradeHelper/common', () => ({ + isActionDisabled: mockIsActionDisabled, + getAPILimit: mockGetAPILimit, + removeLastBuyPrice: mockRemoveLastBuyPrice, + saveOrderStats: mockSaveOrderStats, + saveOverrideAction: mockSaveOverrideAction, + getAndCacheOpenOrdersForSymbol: mockGetAndCacheOpenOrdersForSymbol + })); - mockArchiveSymbolGridTrade = jest.fn().mockResolvedValue({ - profit: 10, - profitPercentage: 0.1, - totalBuyQuoteBuy: 100, - totalSellQuoteQty: 110 - }); + mockArchiveSymbolGridTrade = jest.fn().mockResolvedValue({ + profit: 10, + profitPercentage: 0.1, + totalBuyQuoteBuy: 100, + totalSellQuoteQty: 110 + }); - jest.mock('../../../trailingTradeHelper/configuration', () => ({ - archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade - })); + jest.mock('../../../trailingTradeHelper/configuration', () => ({ + archiveSymbolGridTrade: mockArchiveSymbolGridTrade, + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade + })); - mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); + mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); - jest.mock('../../../trailingTradeHelper/order', () => ({ - getGridTradeOrder: mockGetGridTradeOrder - })); + jest.mock('../../../trailingTradeHelper/order', () => ({ + getGridTradeOrder: mockGetGridTradeOrder + })); - const step = require('../remove-last-buy-price'); + const step = require('../remove-last-buy-price'); - rawData = { - action: 'not-determined', - isLocked: false, - symbol: 'BTCUPUSDT', - symbolConfiguration: { - symbols: ['BTCUSDT', 'BNBUSDT', 'BTCUPUSDT'], - buy: { lastBuyPriceRemoveThreshold: 5 }, - botOptions: { - autoTriggerBuy: { - enabled: false, - triggerAfter: 20 - } - } - }, - symbolInfo: { - filterLotSize: { - stepSize: '0.01000000', - minQty: '0.01000000' - }, - filterMinNotional: { - minNotional: '10.00000000' + rawData = { + action: 'not-determined', + isLocked: false, + symbol: 'BTCUPUSDT', + symbolConfiguration: { + symbols: ['BTCUSDT', 'BNBUSDT', 'BTCUPUSDT'], + buy: { lastBuyPriceRemoveThreshold: 5 }, + botOptions: { + autoTriggerBuy: { + enabled: false, + triggerAfter: 20 } + } + }, + symbolInfo: { + filterLotSize: { + stepSize: '0.01000000', + minQty: '0.01000000' }, - openOrders: [], - baseAssetBalance: { - free: 0, - locked: 0.04 - }, - sell: { - currentPrice: 200, - lastBuyPrice: 160 + filterMinNotional: { + minNotional: '10.00000000' } - }; + }, + openOrders: [], + baseAssetBalance: { + free: 0, + locked: 0.04 + }, + sell: { + currentPrice: 200, + lastBuyPrice: 160 + } + }; - result = await step.execute(loggerMock, rawData); - }); + result = await step.execute(loggerMock, rawData); + }); - it('does not trigger removeLastBuyPrice', () => { - expect(mockRemoveLastBuyPrice).not.toHaveBeenCalled(); - }); + it('does not trigger removeLastBuyPrice', () => { + expect(mockRemoveLastBuyPrice).not.toHaveBeenCalled(); + }); - it('does not trigger archiveSymbolGridTrade', () => { - expect(mockArchiveSymbolGridTrade).not.toHaveBeenCalled(); - }); + it('does not trigger archiveSymbolGridTrade', () => { + expect(mockArchiveSymbolGridTrade).not.toHaveBeenCalled(); + }); - it('does not trigger deleteSymbolGridTrade', () => { - expect(mockDeleteSymbolGridTrade).not.toHaveBeenCalled(); - }); + it('does not trigger deleteSymbolGridTrade', () => { + expect(mockDeleteSymbolGridTrade).not.toHaveBeenCalled(); + }); - it('does not trigger saveOverrideAction', () => { - expect(mockSaveOverrideAction).not.toHaveBeenCalled(); - }); + it('does not trigger saveOverrideAction', () => { + expect(mockSaveOverrideAction).not.toHaveBeenCalled(); + }); - it('does not trigger saveOrderStats', () => { - expect(mockSaveOrderStats).not.toHaveBeenCalled(); - }); + it('does not trigger saveOrderStats', () => { + expect(mockSaveOrderStats).not.toHaveBeenCalled(); + }); - it('returns expected data', () => { - expect(result).toStrictEqual(rawData); - }); + it('returns expected data', () => { + expect(result).toStrictEqual(rawData); }); }); }); @@ -1163,19 +1166,12 @@ describe('remove-last-buy-price.js', () => { totalSellQuoteQty: 110 }); - jest.mock('../../../trailingTradeHelper/configuration', () => ({ - archiveSymbolGridTrade: mockArchiveSymbolGridTrade, - deleteSymbolGridTrade: mockDeleteSymbolGridTrade - })); - mockGetGridTradeOrder = jest.fn().mockResolvedValue(null); jest.mock('../../../trailingTradeHelper/order', () => ({ getGridTradeOrder: mockGetGridTradeOrder })); - const step = require('../remove-last-buy-price'); - rawData = { action: 'not-determined', isLocked: false, @@ -1210,9 +1206,21 @@ describe('remove-last-buy-price.js', () => { } }; + jest.mock('../../../trailingTradeHelper/configuration', () => ({ + archiveSymbolGridTrade: mockArchiveSymbolGridTrade, + deleteSymbolGridTrade: mockDeleteSymbolGridTrade, + getSymbolGridTrade: mockGetSymbolGridTrade + })); + + const step = require('../remove-last-buy-price'); + result = await step.execute(loggerMock, rawData); }); + it('does not trigger binance.client.cancelOrder', () => { + expect(binanceMock.client.cancelOrder).not.toHaveBeenCalled(); + }); + it('does not trigger removeLastBuyPrice', () => { expect(mockRemoveLastBuyPrice).not.toHaveBeenCalled(); }); diff --git a/app/cronjob/trailingTrade/step/remove-last-buy-price.js b/app/cronjob/trailingTrade/step/remove-last-buy-price.js index 0ce6143e..6ec55f79 100644 --- a/app/cronjob/trailingTrade/step/remove-last-buy-price.js +++ b/app/cronjob/trailingTrade/step/remove-last-buy-price.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const moment = require('moment'); -const { slack, PubSub } = require('../../../helpers'); +const { slack, PubSub, binance } = require('../../../helpers'); const { getAPILimit, isActionDisabled, @@ -11,10 +11,71 @@ const { } = require('../../trailingTradeHelper/common'); const { archiveSymbolGridTrade, - deleteSymbolGridTrade + deleteSymbolGridTrade, + getSymbolGridTrade } = require('../../trailingTradeHelper/configuration'); const { getGridTradeOrder } = require('../../trailingTradeHelper/order'); +/** + * Check if base quantity is less than minimum quantity + * + * @param {*} param0 + * @returns + */ +const isLessThanMinimumQuantity = ({ baseAssetQuantity, minQty }) => + baseAssetQuantity <= parseFloat(minQty); + +/** + * Check if remained amount is less than last buy price remove threshold + * + * @param {*} param0 + * @returns + */ +const isLessThanLastBuyPriceRemoveThreshold = ({ + baseAssetQuantity, + currentPrice, + lastBuyPriceRemoveThreshold +}) => baseAssetQuantity * currentPrice < lastBuyPriceRemoveThreshold; + +/** + * Check if sell order is completed + * + * @param {*} logger + * @param {*} symbol + * @returns + */ +const isSellOrderCompleted = async (logger, symbol) => { + // Get trade order + const gridTrade = await getSymbolGridTrade(logger, symbol); + if (_.isEmpty(gridTrade)) { + logger.info('There is no grid trade for the symbol. Do not process.'); + return false; + } + + const { sell } = gridTrade; + if (_.isEmpty(sell)) { + logger.info( + 'There is no sell order in the grid trade for the symbol. Do not process.' + ); + return false; + } + + const allExecuted = _.every(sell, s => s.executed); + if (allExecuted) { + logger.info( + { sell, allExecuted }, + 'All sell orders are executed. Delete last buy price.' + ); + return true; + } + + logger.info( + { sell, allExecuted }, + 'There is unexecuted sell order. Do not process.' + ); + return false; +}; + /** * Set message and return data * @@ -143,6 +204,49 @@ const removeLastBuyPrice = async ( } }; +/** + * Process removing last buy price + * + * @param {*} logger + * @param {*} symbol + * @param {*} data + * @param {*} processMessage + * @param {*} extraMessages + * @returns + */ +const processRemoveLastBuyPrice = async ( + logger, + symbol, + data, + processMessage, + extraMessages +) => { + const refreshedOpenOrders = await getAndCacheOpenOrdersForSymbol( + logger, + symbol + ); + if (refreshedOpenOrders.length > 0) { + await Promise.all( + refreshedOpenOrders.map(order => + binance.client.cancelOrder({ + symbol, + orderId: order.orderId + }) + ) + ); + logger.info( + { refreshedOpenOrders, saveLog: true }, + 'Cancelled all open orders before removing the last buy price.' + ); + // Cache again. + await getAndCacheOpenOrdersForSymbol(logger, symbol); + } + + await removeLastBuyPrice(logger, symbol, data, processMessage, extraMessages); + + return setMessage(logger, data, processMessage); +}; + /** * Remove last buy price if applicable * @@ -184,18 +288,25 @@ const execute = async (logger, rawData) => { return data; } - const gridTradeLastBuyOrder = await getGridTradeLastOrder( - logger, - symbol, - 'buy' + // If last buy price is null, undefined, 0, NaN or less than 0. + if (!lastBuyPrice || lastBuyPrice <= 0) { + logger.info('Do not process because last buy price does not exist.'); + return data; + } + + // If the action is disabled, then do not process. + const checkDisable = await isActionDisabled(symbol); + logger.info( + { tag: 'check-disable', checkDisable }, + 'Checked whether symbol is disabled or not.' ); - if (_.isEmpty(gridTradeLastBuyOrder) === false) { - logger.info( - 'Do not process to remove last buy price because there is a grid trade last buy order to be confirmed.' - ); + + if (checkDisable.isDisabled && checkDisable.canRemoveLastBuyPrice === false) { + logger.info('Do not remove last buy price because action is disabled.'); return data; } + // If there is open order for grid trade sell order, then do not process. const gridTradeLastSellOrder = await getGridTradeLastOrder( logger, symbol, @@ -208,28 +319,21 @@ const execute = async (logger, rawData) => { return data; } - // If last buy price is null, undefined, 0, NaN or less than 0 - if (!lastBuyPrice || lastBuyPrice <= 0) { - logger.info('Do not process because last buy price does not exist.'); - return data; - } + let processMessage = ''; - if (_.isEmpty(openOrders) === false) { - logger.info('Do not remove last buy price in case the order is related.'); - return data; - } + if (await isSellOrderCompleted(logger, symbol)) { + processMessage = 'All sell orders are executed. Delete last buy price.'; - const checkDisable = await isActionDisabled(symbol); - logger.info( - { tag: 'check-disable', checkDisable }, - 'Checked whether symbol is disabled or not.' - ); - if (checkDisable.isDisabled && checkDisable.canRemoveLastBuyPrice === false) { - logger.info('Do not remove last buy price because action is disabled.'); - return data; + logger.info(processMessage); + + return processRemoveLastBuyPrice(logger, symbol, data, processMessage, { + lastBuyPrice, + currentPrice, + minNotional, + openOrders + }); } - // Check one last time for open orders to make sure. const lotPrecision = parseFloat(stepSize) === 1 ? 0 : stepSize.indexOf(1) - 1; const totalBaseAssetBalance = @@ -242,26 +346,16 @@ const execute = async (logger, rawData) => { ) ); - let processMessage = ''; - - let refreshedOpenOrders = []; - if (baseAssetQuantity <= parseFloat(minQty)) { - // Final check for open orders - refreshedOpenOrders = await getAndCacheOpenOrdersForSymbol(logger, symbol); - if (refreshedOpenOrders.length > 0) { - logger.info('Do not remove last buy price. Found open orders.'); - return data; - } - + if (isLessThanMinimumQuantity({ baseAssetQuantity, minQty })) { processMessage = 'Balance is not enough to sell. Delete last buy price.'; - logger.error( + logger.info( { baseAssetQuantity }, processMessage ); - await removeLastBuyPrice(logger, symbol, data, processMessage, { + return processRemoveLastBuyPrice(logger, symbol, data, processMessage, { lastBuyPrice, baseAssetQuantity, minQty, @@ -270,32 +364,27 @@ const execute = async (logger, rawData) => { totalBaseAssetBalance, openOrders }); - - return setMessage(logger, data, processMessage); } - if (baseAssetQuantity * currentPrice < lastBuyPriceRemoveThreshold) { - // Final check for open orders - refreshedOpenOrders = await getAndCacheOpenOrdersForSymbol(logger, symbol); - if (refreshedOpenOrders.length > 0) { - logger.info('Do not remove last buy price. Found open orders.'); - return data; - } - + if ( + isLessThanLastBuyPriceRemoveThreshold({ + baseAssetQuantity, + currentPrice, + lastBuyPriceRemoveThreshold + }) + ) { processMessage = 'Balance is less than the last buy price remove threshold. Delete last buy price.'; - logger.error({ baseAssetQuantity }, processMessage); + logger.info({ baseAssetQuantity }, processMessage); - await removeLastBuyPrice(logger, symbol, data, processMessage, { + return processRemoveLastBuyPrice(logger, symbol, data, processMessage, { lastBuyPrice, baseAssetQuantity, currentPrice, minNotional, openOrders }); - - return setMessage(logger, data, processMessage); } return data;