diff --git a/README.md b/README.md index 52c415c1..cc2f7740 100644 --- a/README.md +++ b/README.md @@ -313,8 +313,8 @@ Or use the frontend to adjust configurations after launching the application. - [x] Fix the bug with handling open orders - [x] Fix the bug with limit step in the frontend - [x] Updated the frontend to display buy open orders with the buy signal +- [x] Add frontend option to update all symbol configurations - [ ] Update the bot to monitor all coins every second -- [ ] Add frontend option to update all symbol configurations - [ ] Add frontend option to disable sorting - [ ] Add minimum required order amount - [ ] Allow browser notification in the frontend @@ -328,6 +328,7 @@ Or use the frontend to adjust configurations after launching the application. - [@OOtta](https://github.com/OOtta) - [@ienthach](https://github.com/ienthach) - [@PlayeTT](https://github.com/PlayeTT) +- [@chopeta](https://github.com/chopeta) ## Contributors diff --git a/app/helpers/__tests__/mongo.test.js b/app/helpers/__tests__/mongo.test.js index 82b998a3..c933465d 100644 --- a/app/helpers/__tests__/mongo.test.js +++ b/app/helpers/__tests__/mongo.test.js @@ -13,6 +13,7 @@ describe('mongo.js', () => { let mockFindOne; let mockInsertOne; let mockUpdateOne; + let mockDeleteMany; let mockDeleteOne; beforeEach(() => { @@ -121,13 +122,13 @@ describe('mongo.js', () => { await mongo.connect(logger); - result = await mongo.findOne(logger, 'simple-stop-chaser-common', { + result = await mongo.findOne(logger, 'trailing-stop-common', { key: 'configuration' }); }); it('triggers database.collection', () => { - expect(mockCollection).toHaveBeenCalledWith('simple-stop-chaser-common'); + expect(mockCollection).toHaveBeenCalledWith('trailing-stop-common'); }); it('triggers collection.findOne', () => { @@ -172,14 +173,14 @@ describe('mongo.js', () => { await mongo.connect(logger); - result = await mongo.insertOne(logger, 'simple-stop-chaser-common', { + result = await mongo.insertOne(logger, 'trailing-stop-common', { key: 'configuration', my: 'value' }); }); it('triggers database.collection', () => { - expect(mockCollection).toHaveBeenCalledWith('simple-stop-chaser-common'); + expect(mockCollection).toHaveBeenCalledWith('trailing-stop-common'); }); it('triggers collection.insertOne', () => { @@ -224,7 +225,7 @@ describe('mongo.js', () => { result = await mongo.upsertOne( logger, - 'simple-stop-chaser-common', + 'trailing-stop-common', { key: 'configuration' }, @@ -236,7 +237,7 @@ describe('mongo.js', () => { }); it('triggers database.collection', () => { - expect(mockCollection).toHaveBeenCalledWith('simple-stop-chaser-common'); + expect(mockCollection).toHaveBeenCalledWith('trailing-stop-common'); }); it('triggers collection.upsertOne', () => { @@ -261,6 +262,54 @@ describe('mongo.js', () => { }); }); + describe('deleteAll', () => { + beforeEach(async () => { + mockDeleteMany = jest.fn().mockResolvedValue(true); + mockCollection = jest.fn(() => ({ + deleteMany: mockDeleteMany + })); + + mockDBCommand = jest.fn().mockResolvedValue(true); + mockDB = jest.fn(() => ({ + command: mockDBCommand, + collection: mockCollection + })); + + mockMongoClient = jest.fn(() => ({ + connect: jest.fn().mockResolvedValue(true), + db: mockDB + })); + + jest.mock('mongodb', () => ({ + MongoClient: mockMongoClient + })); + + require('mongodb'); + + mongo = require('../mongo'); + + await mongo.connect(logger); + + result = await mongo.deleteAll(logger, 'trailing-stop-common', { + key: 'configuration' + }); + }); + + it('triggers database.collection', () => { + expect(mockCollection).toHaveBeenCalledWith('trailing-stop-common'); + }); + + it('triggers collection.deleteMany', () => { + expect(mockDeleteMany).toHaveBeenCalledWith({ + key: 'configuration' + }); + }); + + it('returns expected result', () => { + expect(result).toStrictEqual(true); + }); + }); + describe('deleteOne', () => { beforeEach(async () => { mockDeleteOne = jest.fn().mockResolvedValue(true); @@ -289,13 +338,13 @@ describe('mongo.js', () => { await mongo.connect(logger); - result = await mongo.deleteOne(logger, 'simple-stop-chaser-common', { + result = await mongo.deleteOne(logger, 'trailing-stop-common', { key: 'configuration' }); }); it('triggers database.collection', () => { - expect(mockCollection).toHaveBeenCalledWith('simple-stop-chaser-common'); + expect(mockCollection).toHaveBeenCalledWith('trailing-stop-common'); }); it('triggers collection.deleteOne', () => { diff --git a/app/helpers/mongo.js b/app/helpers/mongo.js index 436ba334..db3fd235 100644 --- a/app/helpers/mongo.js +++ b/app/helpers/mongo.js @@ -69,6 +69,17 @@ const upsertOne = async (funcLogger, collectionName, filter, document) => { return result; }; +const deleteAll = async (funcLogger, collectionName, filter) => { + const logger = funcLogger.child({ helper: 'mongo', funcName: 'deleteAll' }); + + logger.info({ collectionName, filter }, 'Deleting documents from MongoDB'); + const collection = database.collection(collectionName); + const result = collection.deleteMany(filter); + logger.info({ result }, 'Deleted documents from MongoDB'); + + return result; +}; + const deleteOne = async (funcLogger, collectionName, filter) => { const logger = funcLogger.child({ helper: 'mongo', funcName: 'deleteOne' }); @@ -80,4 +91,11 @@ const deleteOne = async (funcLogger, collectionName, filter) => { return result; }; -module.exports = { connect, findOne, insertOne, upsertOne, deleteOne }; +module.exports = { + connect, + findOne, + insertOne, + upsertOne, + deleteAll, + deleteOne +}; diff --git a/app/jobs/trailingTrade/__tests__/configuration.test.js b/app/jobs/trailingTrade/__tests__/configuration.test.js index 5f3a4c62..f4f899bd 100644 --- a/app/jobs/trailingTrade/__tests__/configuration.test.js +++ b/app/jobs/trailingTrade/__tests__/configuration.test.js @@ -159,6 +159,42 @@ describe('configuration.js', () => { }); }); + describe('deleteAllSymbolConfiguration', () => { + beforeEach(async () => { + mongo.deleteAll = jest.fn().mockResolvedValue(true); + + result = await configuration.deleteAllSymbolConfiguration(logger); + }); + + it('trigger mongo.deleteAll', () => { + expect(mongo.deleteAll).toHaveBeenCalledWith( + logger, + 'trailing-trade-symbols', + { + key: { $regex: /^(.+)-configuration/ } + } + ); + }); + }); + + describe('deleteSymbolConfiguration', () => { + beforeEach(async () => { + mongo.deleteOne = jest.fn().mockResolvedValue(true); + + result = await configuration.deleteSymbolConfiguration(logger, 'BTCUSDT'); + }); + + it('trigger mongo.deleteOne', () => { + expect(mongo.deleteOne).toHaveBeenCalledWith( + logger, + 'trailing-trade-symbols', + { + key: `BTCUSDT-configuration` + } + ); + }); + }); + describe('getConfiguration', () => { beforeEach(() => { mongo.upsertOne = jest.fn().mockResolvedValue(true); diff --git a/app/jobs/trailingTrade/configuration.js b/app/jobs/trailingTrade/configuration.js index 4828f299..8cb50f97 100644 --- a/app/jobs/trailingTrade/configuration.js +++ b/app/jobs/trailingTrade/configuration.js @@ -93,6 +93,16 @@ const saveSymbolConfiguration = async ( ); }; +const deleteAllSymbolConfiguration = async logger => + mongo.deleteAll(logger, 'trailing-trade-symbols', { + key: { $regex: /^(.+)-configuration/ } + }); + +const deleteSymbolConfiguration = async (logger, symbol) => + mongo.deleteOne(logger, 'trailing-trade-symbols', { + key: `${symbol}-configuration` + }); + /** * Get global/symbol configuration * @@ -131,5 +141,7 @@ module.exports = { getSymbolConfiguration, saveGlobalConfiguration, saveSymbolConfiguration, + deleteAllSymbolConfiguration, + deleteSymbolConfiguration, getConfiguration }; diff --git a/app/jobs/trailingTrade/step/__tests__/get-indicators.test.js b/app/jobs/trailingTrade/step/__tests__/get-indicators.test.js index d04500b0..38c641bd 100644 --- a/app/jobs/trailingTrade/step/__tests__/get-indicators.test.js +++ b/app/jobs/trailingTrade/step/__tests__/get-indicators.test.js @@ -1,29 +1,37 @@ /* eslint-disable global-require */ -const { binance, mongo, cache, logger } = require('../../../../helpers'); - -const step = require('../get-indicators'); describe('get-indicators.js', () => { let result; let rawData; + let step; + + let binanceMock; + let loggerMock; + + let mockGetLastBuyPrice; describe('execute', () => { beforeEach(() => { - cache.hget = jest.fn().mockResolvedValue(undefined); - cache.hset = jest.fn().mockResolvedValue(true); - mongo.findOne = jest.fn().mockResolvedValue(undefined); - binance.client.candles = jest.fn().mockResolvedValue(); - binance.client.exchangeInfo = jest.fn().mockResolvedValue(); + jest.clearAllMocks().resetModules(); }); - describe('wit no open orders and no last buy price', () => { + describe('with no open orders and no last buy price', () => { beforeEach(async () => { - mongo.findOne = jest.fn().mockResolvedValue(undefined); + const { binance, logger } = require('../../../../helpers'); + binanceMock = binance; + loggerMock = logger; - binance.client.candles = jest + mockGetLastBuyPrice = jest.fn().mockResolvedValue(null); + jest.mock('../../symbol', () => ({ + getLastBuyPrice: mockGetLastBuyPrice + })); + binanceMock.client = {}; + binanceMock.client.candles = jest .fn() .mockResolvedValue(require('./fixtures/binance-candles.json')); + step = require('../get-indicators'); + rawData = { symbol: 'BTCUSDT', symbolConfiguration: { @@ -44,12 +52,8 @@ describe('get-indicators.js', () => { result = await step.execute(logger, rawData); }); - it('triggers mongo.findOne', () => { - expect(mongo.findOne).toHaveBeenCalledWith( - logger, - 'trailing-trade-symbols', - { key: 'BTCUSDT-last-buy-price' } - ); + it('triggers getLastBuyPrice', () => { + expect(mockGetLastBuyPrice).toHaveBeenCalledWith(loggerMock, 'BTCUSDT'); }); it('triggers expected value', () => { @@ -106,14 +110,21 @@ describe('get-indicators.js', () => { describe('with no open orders and last buy price', () => { beforeEach(async () => { - mongo.findOne = jest.fn().mockResolvedValue({ - lastBuyPrice: 9000 - }); + const { binance, logger } = require('../../../../helpers'); + binanceMock = binance; + loggerMock = logger; - binance.client.candles = jest + mockGetLastBuyPrice = jest.fn().mockResolvedValue(9000); + jest.mock('../../symbol', () => ({ + getLastBuyPrice: mockGetLastBuyPrice + })); + binanceMock.client = {}; + binanceMock.client.candles = jest .fn() .mockResolvedValue(require('./fixtures/binance-candles.json')); + step = require('../get-indicators'); + rawData = { symbol: 'BTCUSDT', symbolConfiguration: { @@ -164,12 +175,8 @@ describe('get-indicators.js', () => { result = await step.execute(logger, rawData); }); - it('triggers mongo.findOne', () => { - expect(mongo.findOne).toHaveBeenCalledWith( - logger, - 'trailing-trade-symbols', - { key: 'BTCUSDT-last-buy-price' } - ); + it('triggers getLastBuyPrice', () => { + expect(mockGetLastBuyPrice).toHaveBeenCalledWith(loggerMock, 'BTCUSDT'); }); it('triggers expected value', () => { @@ -319,12 +326,20 @@ describe('get-indicators.js', () => { describe('with open orders and no last buy price', () => { beforeEach(async () => { - mongo.findOne = jest.fn().mockResolvedValue(undefined); + const { binance, logger } = require('../../../../helpers'); + binanceMock = binance; + loggerMock = logger; - binance.client.candles = jest + mockGetLastBuyPrice = jest.fn().mockResolvedValue(null); + jest.mock('../../symbol', () => ({ + getLastBuyPrice: mockGetLastBuyPrice + })); + binanceMock.client = {}; + binanceMock.client.candles = jest .fn() .mockResolvedValue(require('./fixtures/binance-candles.json')); + step = require('../get-indicators'); rawData = { symbol: 'BTCUSDT', symbolConfiguration: { @@ -524,12 +539,21 @@ describe('get-indicators.js', () => { describe('with balance is not found', () => { beforeEach(async () => { - mongo.findOne = jest.fn().mockResolvedValue(undefined); + const { binance, logger } = require('../../../../helpers'); + binanceMock = binance; + loggerMock = logger; - binance.client.candles = jest + mockGetLastBuyPrice = jest.fn().mockResolvedValue(null); + jest.mock('../../symbol', () => ({ + getLastBuyPrice: mockGetLastBuyPrice + })); + binanceMock.client = {}; + binanceMock.client.candles = jest .fn() .mockResolvedValue(require('./fixtures/binance-candles.json')); + step = require('../get-indicators'); + rawData = { symbol: 'BTCUSDT', symbolConfiguration: { diff --git a/app/jobs/trailingTrade/step/__tests__/place-buy-order.test.js b/app/jobs/trailingTrade/step/__tests__/place-buy-order.test.js index 8282490b..94bb4491 100644 --- a/app/jobs/trailingTrade/step/__tests__/place-buy-order.test.js +++ b/app/jobs/trailingTrade/step/__tests__/place-buy-order.test.js @@ -26,7 +26,7 @@ describe('place-buy-order.js', () => { }, symbolConfiguration: { buy: { - enabed: true, + enabled: true, maxPurchaseAmount: 50, stopPercentage: 1.01, limitPercentage: 1.011 @@ -62,7 +62,7 @@ describe('place-buy-order.js', () => { }, symbolConfiguration: { buy: { - enabed: true, + enabled: true, maxPurchaseAmount: 50, stopPercentage: 1.01, limitPercentage: 1.011 @@ -130,7 +130,7 @@ describe('place-buy-order.js', () => { }, symbolConfiguration: { buy: { - enabed: true, + enabled: true, maxPurchaseAmount: 50, stopPercentage: 1.01, limitPercentage: 1.011 @@ -180,7 +180,7 @@ describe('place-buy-order.js', () => { }, symbolConfiguration: { buy: { - enabed: true, + enabled: true, maxPurchaseAmount: 50, stopPercentage: 1.01, limitPercentage: 1.011 @@ -230,7 +230,7 @@ describe('place-buy-order.js', () => { }, symbolConfiguration: { buy: { - enabed: false, + enabled: false, maxPurchaseAmount: 50, stopPercentage: 1.01, limitPercentage: 1.011 @@ -280,7 +280,7 @@ describe('place-buy-order.js', () => { }, symbolConfiguration: { buy: { - enabed: true, + enabled: true, maxPurchaseAmount: 50, stopPercentage: 1.01, limitPercentage: 1.011 diff --git a/app/jobs/trailingTrade/step/__tests__/place-sell-order.test.js b/app/jobs/trailingTrade/step/__tests__/place-sell-order.test.js index ca8244fd..35ac67cd 100644 --- a/app/jobs/trailingTrade/step/__tests__/place-sell-order.test.js +++ b/app/jobs/trailingTrade/step/__tests__/place-sell-order.test.js @@ -22,7 +22,11 @@ describe('place-sell-order.js', () => { filterMinNotional: { minNotional: '10.00000000' } }, symbolConfiguration: { - sell: { enabed: true, stopPercentage: 0.99, limitPercentage: 0.989 } + sell: { + enabled: true, + stopPercentage: 0.99, + limitPercentage: 0.989 + } }, action: 'not-determined', baseAssetBalance: { free: 0.5 }, @@ -51,7 +55,11 @@ describe('place-sell-order.js', () => { filterMinNotional: { minNotional: '10.00000000' } }, symbolConfiguration: { - sell: { enabed: true, stopPercentage: 0.99, limitPercentage: 0.989 } + sell: { + enabled: true, + stopPercentage: 0.99, + limitPercentage: 0.989 + } }, action: 'sell', baseAssetBalance: { free: 0.5 }, @@ -112,7 +120,11 @@ describe('place-sell-order.js', () => { filterMinNotional: { minNotional: '10.00000000' } }, symbolConfiguration: { - sell: { enabed: true, stopPercentage: 0.99, limitPercentage: 0.989 } + sell: { + enabled: true, + stopPercentage: 0.99, + limitPercentage: 0.989 + } }, action: 'sell', baseAssetBalance: { free: 0.01 }, @@ -155,7 +167,11 @@ describe('place-sell-order.js', () => { filterMinNotional: { minNotional: '10.00000000' } }, symbolConfiguration: { - sell: { enabed: true, stopPercentage: 0.99, limitPercentage: 0.989 } + sell: { + enabled: true, + stopPercentage: 0.99, + limitPercentage: 0.989 + } }, action: 'sell', baseAssetBalance: { free: 0.05 }, @@ -199,7 +215,7 @@ describe('place-sell-order.js', () => { }, symbolConfiguration: { sell: { - enabed: false, + enabled: false, stopPercentage: 0.99, limitPercentage: 0.989 } @@ -246,7 +262,7 @@ describe('place-sell-order.js', () => { }, symbolConfiguration: { sell: { - enabed: true, + enabled: true, stopPercentage: 0.99, limitPercentage: 0.989 } diff --git a/app/jobs/trailingTrade/step/get-indicators.js b/app/jobs/trailingTrade/step/get-indicators.js index fe6cf87e..7274322a 100644 --- a/app/jobs/trailingTrade/step/get-indicators.js +++ b/app/jobs/trailingTrade/step/get-indicators.js @@ -1,7 +1,8 @@ /* eslint-disable prefer-destructuring */ const _ = require('lodash'); const moment = require('moment'); -const { binance, mongo } = require('../../../helpers'); +const { binance } = require('../../../helpers'); +const { getLastBuyPrice } = require('../symbol'); /** * Flatten candle data @@ -23,27 +24,6 @@ const flattenCandlesData = candles => { }; }; -/** - * Get last buy price from mongodb - * - * @param {*} logger - * @param {*} symbol - */ -const getLastBuyPrice = async (logger, symbol) => { - const lastBuyPriceDoc = await mongo.findOne( - logger, - 'trailing-trade-symbols', - { - key: `${symbol}-last-buy-price` - } - ); - - const cachedLastBuyPrice = _.get(lastBuyPriceDoc, 'lastBuyPrice', null); - logger.debug({ cachedLastBuyPrice }, 'Last buy price'); - - return cachedLastBuyPrice; -}; - /** * Get symbol information, buy/sell indicators * diff --git a/app/jobs/trailingTrade/step/place-buy-order.js b/app/jobs/trailingTrade/step/place-buy-order.js index 6d0c34a6..63119eb7 100644 --- a/app/jobs/trailingTrade/step/place-buy-order.js +++ b/app/jobs/trailingTrade/step/place-buy-order.js @@ -23,7 +23,7 @@ const execute = async (logger, rawData) => { }, symbolConfiguration: { buy: { - enabed: tradingEnabled, + enabled: tradingEnabled, maxPurchaseAmount, stopPercentage, limitPercentage @@ -92,7 +92,7 @@ const execute = async (logger, rawData) => { return data; } - if (tradingEnabled === false) { + if (tradingEnabled !== true) { data.buy.processMessage = `Trading for ${symbol} is disabled. Do not place an order.`; data.buy.updatedAt = moment().utc(); diff --git a/app/jobs/trailingTrade/step/place-sell-order.js b/app/jobs/trailingTrade/step/place-sell-order.js index 002f1c17..eedcbbc2 100644 --- a/app/jobs/trailingTrade/step/place-sell-order.js +++ b/app/jobs/trailingTrade/step/place-sell-order.js @@ -20,7 +20,7 @@ const execute = async (logger, rawData) => { filterMinNotional: { minNotional } }, symbolConfiguration: { - sell: { enabed: tradingEnabled, stopPercentage, limitPercentage } + sell: { enabled: tradingEnabled, stopPercentage, limitPercentage } }, action, baseAssetBalance: { free: baseAssetFreeBalance }, @@ -68,7 +68,7 @@ const execute = async (logger, rawData) => { return data; } - if (tradingEnabled === false) { + if (tradingEnabled !== true) { data.sell.processMessage = `Trading for ${symbol} is disabled. Do not place an order.`; data.sell.updatedAt = moment().utc(); diff --git a/app/jobs/trailingTrade/symbol.js b/app/jobs/trailingTrade/symbol.js new file mode 100644 index 00000000..38fb7993 --- /dev/null +++ b/app/jobs/trailingTrade/symbol.js @@ -0,0 +1,25 @@ +const _ = require('lodash'); +const { mongo } = require('../../helpers'); + +/** + * Get last buy price from mongodb + * + * @param {*} logger + * @param {*} symbol + */ +const getLastBuyPrice = async (logger, symbol) => { + const lastBuyPriceDoc = await mongo.findOne( + logger, + 'trailing-trade-symbols', + { + key: `${symbol}-last-buy-price` + } + ); + + const cachedLastBuyPrice = _.get(lastBuyPriceDoc, 'lastBuyPrice', null); + logger.debug({ cachedLastBuyPrice }, 'Last buy price'); + + return cachedLastBuyPrice; +}; + +module.exports = { getLastBuyPrice }; diff --git a/app/websocket/__tests__/configure.test.js b/app/websocket/__tests__/configure.test.js index affc5c39..f96012b7 100644 --- a/app/websocket/__tests__/configure.test.js +++ b/app/websocket/__tests__/configure.test.js @@ -17,6 +17,7 @@ describe('websocket/configure.js', () => { let mockHandleSymbolUpdate; let mockHandleSymbolDelete; let mockHandleSymbolSettingUpdate; + let mockHandleSymbolSettingDelete; let wss; let config; @@ -80,13 +81,15 @@ describe('websocket/configure.js', () => { mockHandleSymbolUpdate = jest.fn().mockResolvedValue(true); mockHandleSymbolDelete = jest.fn().mockResolvedValue(true); mockHandleSymbolSettingUpdate = jest.fn().mockResolvedValue(true); + mockHandleSymbolSettingDelete = jest.fn().mockResolvedValue(true); jest.mock('../handlers', () => ({ handleLatest: mockHandleLatest, handleSettingUpdate: mockHandleSettingUpdate, handleSymbolUpdate: mockHandleSymbolUpdate, handleSymbolDelete: mockHandleSymbolDelete, - handleSymbolSettingUpdate: mockHandleSymbolSettingUpdate + handleSymbolSettingUpdate: mockHandleSymbolSettingUpdate, + handleSymbolSettingDelete: mockHandleSymbolSettingDelete })); mockExpressServerOn = jest.fn().mockImplementation((_event, cb) => { @@ -518,4 +521,52 @@ describe('websocket/configure.js', () => { expect(wss).not.toBeNull(); }); }); + + describe('when message command is symbol-setting-delete', () => { + beforeEach(() => { + mockWebSocketServerWebSocketOn = jest + .fn() + .mockImplementation((_event, cb) => { + cb( + JSON.stringify({ + command: 'symbol-setting-delete' + }) + ); + }); + + mockWebSocketServerWebSocketSend = jest.fn().mockReturnValue(true); + + mockWebSocketServerOn = jest.fn().mockImplementation((_event, cb) => { + cb({ + on: mockWebSocketServerWebSocketOn, + send: mockWebSocketServerWebSocketSend + }); + }); + + WebSocket.Server.mockImplementation(() => ({ + on: mockWebSocketServerOn, + handleUpgrade: mockWebSocketServerHandleUpgrade, + emit: mockWebSocketServerEmit + })); + + const { logger } = require('../../helpers'); + + const { configureWebSocket } = require('../configure'); + configureWebSocket(mockExpressServer, logger); + }); + + it('triggers handleSymbolSettingDelete', () => { + expect(mockHandleSymbolSettingDelete).toHaveBeenCalledWith( + expect.any(Object), + expect.any(Object), + { + command: 'symbol-setting-delete' + } + ); + }); + + it('returns wss', () => { + expect(wss).not.toBeNull(); + }); + }); }); diff --git a/app/websocket/configure.js b/app/websocket/configure.js index 45b6c5d0..bba66ec8 100644 --- a/app/websocket/configure.js +++ b/app/websocket/configure.js @@ -4,7 +4,8 @@ const { handleSettingUpdate, handleSymbolUpdate, handleSymbolDelete, - handleSymbolSettingUpdate + handleSymbolSettingUpdate, + handleSymbolSettingDelete } = require('./handlers'); const handleWarning = (logger, ws, message) => { @@ -57,6 +58,9 @@ const configureWebSocket = async (server, funcLogger) => { case 'symbol-setting-update': await handleSymbolSettingUpdate(commandLogger, ws, payload); break; + case 'symbol-setting-delete': + await handleSymbolSettingDelete(commandLogger, ws, payload); + break; default: handleWarning(logger, ws, 'Command is not recognised.'); } diff --git a/app/websocket/handlers/__tests__/fixtures/latest-stats.json b/app/websocket/handlers/__tests__/fixtures/latest-stats.json index 5aef003e..363c0add 100644 --- a/app/websocket/handlers/__tests__/fixtures/latest-stats.json +++ b/app/websocket/handlers/__tests__/fixtures/latest-stats.json @@ -1,9 +1,17 @@ { "result": true, "type": "latest", - "configuration": { "enabled": true, "candles": { "interval": "15m" } }, + "configuration": { + "enabled": true, + "type": "i-am-global", + "candles": { "interval": "15m" } + }, "common": { - "configuration": { "enabled": true, "candles": { "interval": "15m" } }, + "configuration": { + "enabled": true, + "type": "i-am-global", + "candles": { "interval": "15m" } + }, "accountInfo": { "makerCommission": 0, "takerCommission": 0, @@ -36,52 +44,19 @@ "publicURL": "https://chatty-robin-84.loca.lt" }, "stats": { - "symbols": { - "BNBUSDT": { + "symbols": [ + { "globalConfiguration": { - "_id": "604d5e1b485f808080e07b7d", - "key": "configuration", "enabled": true, - "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "ETHUSDT", "BNBUSDT"], - "supportFIATs": ["USDT"], - "candles": { "interval": "1m", "limit": 10 }, - "buy": { - "enabled": true, - "maxPurchaseAmount": 100, - "triggerPercentage": 1, - "stopPercentage": 1.01, - "limitPercentage": 1.011 - }, - "sell": { - "enabled": true, - "triggerPercentage": 1.01, - "stopPercentage": 0.99, - "limitPercentage": 0.989 - } + "type": "i-am-global", + "candles": { "interval": "15m" } }, "symbol": "BNBUSDT", "symbolConfiguration": { - "_id": "604d5e1b485f808080e07b7d", - "key": "configuration", "enabled": true, - "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "ETHUSDT", "BNBUSDT"], - "supportFIATs": ["USDT"], - "candles": { "interval": "1m", "limit": 10 }, - "buy": { - "enabled": true, - "maxPurchaseAmount": 100, - "triggerPercentage": 1, - "stopPercentage": 1.01, - "limitPercentage": 1.011 - }, - "sell": { - "enabled": true, - "triggerPercentage": 1.01, - "stopPercentage": 0.99, - "limitPercentage": 0.989 - } + "symbol": "BNBUSDT", + "type": "i-am-symbol", + "candles": { "interval": "15m" } }, "accountInfo": { "makerCommission": 0, @@ -208,51 +183,18 @@ "updatedAt": "2021-03-14T00:51:53.577Z" } }, - "ETHUSDT": { + { "globalConfiguration": { - "_id": "604d5e1b485f808080e07b7d", - "key": "configuration", "enabled": true, - "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "ETHUSDT", "BNBUSDT"], - "supportFIATs": ["USDT"], - "candles": { "interval": "1m", "limit": 10 }, - "buy": { - "enabled": true, - "maxPurchaseAmount": 100, - "triggerPercentage": 1, - "stopPercentage": 1.01, - "limitPercentage": 1.011 - }, - "sell": { - "enabled": true, - "triggerPercentage": 1.01, - "stopPercentage": 0.99, - "limitPercentage": 0.989 - } + "type": "i-am-global", + "candles": { "interval": "15m" } }, "symbol": "ETHUSDT", "symbolConfiguration": { - "_id": "604d5e1b485f808080e07b7d", - "key": "configuration", "enabled": true, - "cronTime": "* * * * * *", - "symbols": ["BTCUSDT", "ETHUSDT", "BNBUSDT"], - "supportFIATs": ["USDT"], - "candles": { "interval": "1m", "limit": 10 }, - "buy": { - "enabled": true, - "maxPurchaseAmount": 100, - "triggerPercentage": 1, - "stopPercentage": 1.01, - "limitPercentage": 1.011 - }, - "sell": { - "enabled": true, - "triggerPercentage": 1.01, - "stopPercentage": 0.99, - "limitPercentage": 0.989 - } + "symbol": "ETHUSDT", + "type": "i-am-symbol", + "candles": { "interval": "15m" } }, "accountInfo": { "makerCommission": 0, @@ -435,6 +377,6 @@ "updatedAt": "2021-03-14T00:51:52.864Z" } } - } + ] } } diff --git a/app/websocket/handlers/__tests__/index.test.js b/app/websocket/handlers/__tests__/index.test.js index 8d11c231..edaed352 100644 --- a/app/websocket/handlers/__tests__/index.test.js +++ b/app/websocket/handlers/__tests__/index.test.js @@ -7,7 +7,8 @@ describe('index', () => { handleSettingUpdate: expect.any(Function), handleSymbolUpdate: expect.any(Function), handleSymbolDelete: expect.any(Function), - handleSymbolSettingUpdate: expect.any(Function) + handleSymbolSettingUpdate: expect.any(Function), + handleSymbolSettingDelete: expect.any(Function) }); }); }); diff --git a/app/websocket/handlers/__tests__/latest.test.js b/app/websocket/handlers/__tests__/latest.test.js index e60c650e..3735d690 100644 --- a/app/websocket/handlers/__tests__/latest.test.js +++ b/app/websocket/handlers/__tests__/latest.test.js @@ -38,6 +38,33 @@ describe('latest.test.js', () => { return { enabled: true }; } + if ( + collection === 'trailing-trade-symbols' && + _.isEqual(filter, { key: 'BNBUSDT-configuration' }) + ) { + return { enabled: true, symbol: 'BNBUSDT' }; + } + if ( + collection === 'trailing-trade-symbols' && + _.isEqual(filter, { key: 'BNBUSDT-last-buy-price' }) + ) { + return { lastBuyPrice: null }; + } + + if ( + collection === 'trailing-trade-symbols' && + _.isEqual(filter, { key: 'ETHUSDT-configuration' }) + ) { + return { enabled: true, symbol: 'ETHUSDT' }; + } + + if ( + collection === 'trailing-trade-symbols' && + _.isEqual(filter, { key: 'ETHUSDT-last-buy-price' }) + ) { + return { lastBuyPrice: null }; + } + return null; }); @@ -75,7 +102,38 @@ describe('latest.test.js', () => { collection === 'trailing-trade-common' && _.isEqual(filter, { key: 'configuration' }) ) { - return { enabled: true, candles: { interval: '15m' } }; + return { + enabled: true, + type: 'i-am-global', + candles: { interval: '15m' } + }; + } + + if ( + collection === 'trailing-trade-symbols' && + _.isEqual(filter, { key: 'BNBUSDT-configuration' }) + ) { + return { enabled: true, symbol: 'BNBUSDT', type: 'i-am-symbol' }; + } + if ( + collection === 'trailing-trade-symbols' && + _.isEqual(filter, { key: 'BNBUSDT-last-buy-price' }) + ) { + return { lastBuyPrice: null, type: 'i-am-symbol' }; + } + + if ( + collection === 'trailing-trade-symbols' && + _.isEqual(filter, { key: 'ETHUSDT-configuration' }) + ) { + return { enabled: true, symbol: 'ETHUSDT', type: 'i-am-symbol' }; + } + + if ( + collection === 'trailing-trade-symbols' && + _.isEqual(filter, { key: 'ETHUSDT-last-buy-price' }) + ) { + return { lastBuyPrice: null, type: 'i-am-symbol' }; } return null; diff --git a/app/websocket/handlers/__tests__/setting-update.test.js b/app/websocket/handlers/__tests__/setting-update.test.js index 8939764f..1ab57272 100644 --- a/app/websocket/handlers/__tests__/setting-update.test.js +++ b/app/websocket/handlers/__tests__/setting-update.test.js @@ -4,6 +4,7 @@ const _ = require('lodash'); describe('setting-update.test.js', () => { let mockWebSocketServer; let mockWebSocketServerWebSocketSend; + let mockDeleteAllSymbolConfiguration; let cacheMock; let mongoMock; @@ -12,6 +13,12 @@ describe('setting-update.test.js', () => { beforeEach(() => { jest.clearAllMocks().resetModules(); + mockDeleteAllSymbolConfiguration = jest.fn().mockResolvedValue(true); + + jest.mock('../../../jobs/trailingTrade/configuration', () => ({ + deleteAllSymbolConfiguration: mockDeleteAllSymbolConfiguration + })); + mockWebSocketServerWebSocketSend = jest.fn().mockResolvedValue(true); mockWebSocketServer = { @@ -65,125 +72,241 @@ describe('setting-update.test.js', () => { it('does not trigger cache.hdel', () => { expect(cacheMock.hdel).not.toHaveBeenCalled(); }); + + it('does not trigger deleteAllSymbolConfiguration', () => { + expect(mockDeleteAllSymbolConfiguration).not.toHaveBeenCalled(); + }); }); describe('when configuration is valid', () => { - beforeEach(async () => { - const { mongo, logger, cache } = require('../../../helpers'); - mockLogger = logger; - cacheMock = cache; - mongoMock = mongo; + describe('when apply to all', () => { + beforeEach(async () => { + const { mongo, logger, cache } = require('../../../helpers'); + mockLogger = logger; + cacheMock = cache; + mongoMock = mongo; - cacheMock.hdel = jest.fn().mockResolvedValue(true); + cacheMock.hdel = jest.fn().mockResolvedValue(true); - mongoMock.upsertOne = jest.fn().mockResolvedValue(true); - mongoMock.findOne = jest - .fn() - .mockImplementation((_logger, collection, filter) => { - if ( - collection === 'trailing-trade-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return { + mongoMock.upsertOne = jest.fn().mockResolvedValue(true); + mongoMock.findOne = jest + .fn() + .mockImplementation((_logger, collection, filter) => { + if ( + collection === 'trailing-trade-common' && + _.isEqual(filter, { key: 'configuration' }) + ) { + return { + enabled: true, + symbols: ['BTCUSDT'], + supportFIATs: ['USDT'], + candles: { + interval: '1d', + limit: '10' + }, + buy: { + enabled: true, + maxPurchaseAmount: 100 + }, + sell: { + enabled: true, + lastbuyPercentage: 1.06, + stopPercentage: 0.99, + limitPercentage: 0.98 + } + }; + } + return ''; + }); + + const { handleSettingUpdate } = require('../setting-update'); + await handleSettingUpdate(logger, mockWebSocketServer, { + data: { + action: 'apply-to-all', + enabled: true, + symbols: ['BTCUSDT', 'LTCUSDT'], + supportFIATs: ['USDT', 'BUSD'], + candles: { + interval: '1h', + limit: '100' + }, + buy: { enabled: true, - symbols: ['BTCUSDT'], - supportFIATs: ['USDT'], - candles: { - interval: '1d', - limit: '10' - }, + maxPurchaseAmount: 150 + }, + sell: { + enabled: true, + lastbuyPercentage: 1.07, + stopPercentage: 0.98, + limitPercentage: 0.97 + } + } + }); + }); + + it('triggers mongo.findOne', () => { + expect(mongoMock.findOne).toHaveBeenCalledWith( + mockLogger, + 'trailing-trade-common', + { key: 'configuration' } + ); + }); + + it('triggers mongo.upsertOne', () => { + expect(mongoMock.upsertOne).toHaveBeenCalledWith( + mockLogger, + 'trailing-trade-common', + { key: 'configuration' }, + { + key: 'configuration', + enabled: true, + symbols: ['BTCUSDT', 'LTCUSDT'], + supportFIATs: ['USDT', 'BUSD'], + candles: { + interval: '1h', + limit: '100' + }, + buy: { + enabled: true, + maxPurchaseAmount: 150 + }, + sell: { + enabled: true, + lastbuyPercentage: 1.07, + stopPercentage: 0.98, + limitPercentage: 0.97 + } + } + ); + }); + + it('triggers cache.hdel', () => { + expect(cacheMock.hdel).toHaveBeenCalledWith( + 'trailing-trade-common', + 'exchange-symbols' + ); + }); + + it('triggers deleteAllSymbolConfiguration', () => { + expect(mockDeleteAllSymbolConfiguration).toHaveBeenCalledWith( + mockLogger + ); + }); + + it('triggers ws.send', () => { + expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( + JSON.stringify({ + result: true, + type: 'setting-update-result', + newConfiguration: { + action: 'apply-to-all', + enabled: true, + symbols: ['BTCUSDT', 'LTCUSDT'], + supportFIATs: ['USDT', 'BUSD'], + candles: { interval: '1h', limit: '100' }, buy: { enabled: true, - maxPurchaseAmount: 100 + maxPurchaseAmount: 150 }, sell: { enabled: true, - lastbuyPercentage: 1.06, - stopPercentage: 0.99, - limitPercentage: 0.98 + lastbuyPercentage: 1.07, + stopPercentage: 0.98, + limitPercentage: 0.97 } - }; - } - return ''; - }); - - const { handleSettingUpdate } = require('../setting-update'); - await handleSettingUpdate(logger, mockWebSocketServer, { - data: { - enabeld: false, - symbols: ['BTCUSDT', 'LTCUSDT'], - supportFIATs: ['USDT', 'BUSD'], - candles: { - interval: '1h', - limit: '100' - }, - buy: { - enabled: true, - maxPurchaseAmount: 150 - }, - sell: { - enabled: true, - lastbuyPercentage: 1.07, - stopPercentage: 0.98, - limitPercentage: 0.97 - } - } + } + }) + ); }); }); - it('triggers mongo.findOne', () => { - expect(mongoMock.findOne).toHaveBeenCalledWith( - mockLogger, - 'trailing-trade-common', - { key: 'configuration' } - ); - }); + describe('when apply to only global', () => { + beforeEach(async () => { + const { mongo, logger, cache } = require('../../../helpers'); + mockLogger = logger; + cacheMock = cache; + mongoMock = mongo; - it('triggers mongo.upsertOne', () => { - expect(mongoMock.upsertOne).toHaveBeenCalledWith( - mockLogger, - 'trailing-trade-common', - { key: 'configuration' }, - { - key: 'configuration', - enabled: true, - symbols: ['BTCUSDT', 'LTCUSDT'], - supportFIATs: ['USDT', 'BUSD'], - candles: { - interval: '1h', - limit: '100' - }, - buy: { - enabled: true, - maxPurchaseAmount: 150 - }, - sell: { + cacheMock.hdel = jest.fn().mockResolvedValue(true); + + mongoMock.upsertOne = jest.fn().mockResolvedValue(true); + mongoMock.findOne = jest + .fn() + .mockImplementation((_logger, collection, filter) => { + if ( + collection === 'trailing-trade-common' && + _.isEqual(filter, { key: 'configuration' }) + ) { + return { + enabled: true, + symbols: ['BTCUSDT'], + supportFIATs: ['USDT'], + candles: { + interval: '1d', + limit: '10' + }, + buy: { + enabled: true, + maxPurchaseAmount: 100 + }, + sell: { + enabled: true, + lastbuyPercentage: 1.06, + stopPercentage: 0.99, + limitPercentage: 0.98 + } + }; + } + return ''; + }); + + const { handleSettingUpdate } = require('../setting-update'); + await handleSettingUpdate(logger, mockWebSocketServer, { + data: { + action: 'apply-to-global-only', enabled: true, - lastbuyPercentage: 1.07, - stopPercentage: 0.98, - limitPercentage: 0.97 + symbols: ['BTCUSDT', 'LTCUSDT'], + supportFIATs: ['USDT', 'BUSD'], + candles: { + interval: '1h', + limit: '100' + }, + buy: { + enabled: true, + maxPurchaseAmount: 150 + }, + sell: { + enabled: true, + lastbuyPercentage: 1.07, + stopPercentage: 0.98, + limitPercentage: 0.97 + } } - } - ); - }); + }); + }); - it('triggersd cache.hdel', () => { - expect(cacheMock.hdel).toHaveBeenCalledWith( - 'trailing-trade-common', - 'exchange-symbols' - ); - }); + it('triggers mongo.findOne', () => { + expect(mongoMock.findOne).toHaveBeenCalledWith( + mockLogger, + 'trailing-trade-common', + { key: 'configuration' } + ); + }); - it('triggers ws.send', () => { - expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( - JSON.stringify({ - result: true, - type: 'setting-update-result', - newConfiguration: { + it('triggers mongo.upsertOne', () => { + expect(mongoMock.upsertOne).toHaveBeenCalledWith( + mockLogger, + 'trailing-trade-common', + { key: 'configuration' }, + { + key: 'configuration', enabled: true, symbols: ['BTCUSDT', 'LTCUSDT'], supportFIATs: ['USDT', 'BUSD'], - candles: { interval: '1h', limit: '100' }, + candles: { + interval: '1h', + limit: '100' + }, buy: { enabled: true, maxPurchaseAmount: 150 @@ -195,8 +318,47 @@ describe('setting-update.test.js', () => { limitPercentage: 0.97 } } - }) - ); + ); + }); + + it('triggers cache.hdel', () => { + expect(cacheMock.hdel).toHaveBeenCalledWith( + 'trailing-trade-common', + 'exchange-symbols' + ); + }); + + it('does not trigger deleteAllSymbolConfiguration', () => { + expect(mockDeleteAllSymbolConfiguration).not.toHaveBeenCalledWith( + mockLogger + ); + }); + + it('triggers ws.send', () => { + expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( + JSON.stringify({ + result: true, + type: 'setting-update-result', + newConfiguration: { + action: 'apply-to-global-only', + enabled: true, + symbols: ['BTCUSDT', 'LTCUSDT'], + supportFIATs: ['USDT', 'BUSD'], + candles: { interval: '1h', limit: '100' }, + buy: { + enabled: true, + maxPurchaseAmount: 150 + }, + sell: { + enabled: true, + lastbuyPercentage: 1.07, + stopPercentage: 0.98, + limitPercentage: 0.97 + } + } + }) + ); + }); }); }); }); diff --git a/app/websocket/handlers/__tests__/symbol-setting-delete.test.js b/app/websocket/handlers/__tests__/symbol-setting-delete.test.js new file mode 100644 index 00000000..0e2e3264 --- /dev/null +++ b/app/websocket/handlers/__tests__/symbol-setting-delete.test.js @@ -0,0 +1,56 @@ +/* eslint-disable global-require */ + +describe('symbol-setting-delete.test.js', () => { + let mockWebSocketServer; + let mockWebSocketServerWebSocketSend; + + let mockLogger; + + let mockDeleteSymbolConfiguration; + + beforeEach(() => { + jest.clearAllMocks().resetModules(); + + mockWebSocketServerWebSocketSend = jest.fn().mockResolvedValue(true); + + mockWebSocketServer = { + send: mockWebSocketServerWebSocketSend + }; + }); + + describe('when symbol is provided', () => { + beforeEach(async () => { + const { logger } = require('../../../helpers'); + mockLogger = logger; + + mockDeleteSymbolConfiguration = jest.fn().mockResolvedValue(true); + + jest.mock('../../../jobs/trailingTrade/configuration', () => ({ + deleteSymbolConfiguration: mockDeleteSymbolConfiguration + })); + + const { handleSymbolSettingDelete } = require('../symbol-setting-delete'); + await handleSymbolSettingDelete(logger, mockWebSocketServer, { + data: { + symbol: 'BTCUSDT' + } + }); + }); + + it('triggers deleteSymbolConfiguration', () => { + expect(mockDeleteSymbolConfiguration).toHaveBeenCalledWith( + mockLogger, + 'BTCUSDT' + ); + }); + + it('triggers ws.send', () => { + expect(mockWebSocketServerWebSocketSend).toHaveBeenCalledWith( + JSON.stringify({ + result: true, + type: 'symbol-setting-delete-result' + }) + ); + }); + }); +}); diff --git a/app/websocket/handlers/index.js b/app/websocket/handlers/index.js index 7d1f71b9..288602c8 100644 --- a/app/websocket/handlers/index.js +++ b/app/websocket/handlers/index.js @@ -3,11 +3,13 @@ const { handleSettingUpdate } = require('./setting-update'); const { handleSymbolUpdate } = require('./symbol-update'); const { handleSymbolDelete } = require('./symbol-delete'); const { handleSymbolSettingUpdate } = require('./symbol-setting-update'); +const { handleSymbolSettingDelete } = require('./symbol-setting-delete'); module.exports = { handleLatest, handleSettingUpdate, handleSymbolUpdate, handleSymbolDelete, - handleSymbolSettingUpdate + handleSymbolSettingUpdate, + handleSymbolSettingDelete }; diff --git a/app/websocket/handlers/latest.js b/app/websocket/handlers/latest.js index e0e92019..b486871c 100644 --- a/app/websocket/handlers/latest.js +++ b/app/websocket/handlers/latest.js @@ -2,9 +2,12 @@ const _ = require('lodash'); const { cache } = require('../../helpers'); const { - getGlobalConfiguration + getGlobalConfiguration, + getConfiguration } = require('../../jobs/trailingTrade/configuration'); +const { getLastBuyPrice } = require('../../jobs/trailingTrade/symbol'); + const getSymbolFromKey = key => { const fragments = key.split('-'); const symbol = fragments[0]; @@ -29,13 +32,13 @@ const handleLatest = async (logger, ws, _payload) => { symbols: {} }; - const configuration = await getGlobalConfiguration(logger); - logger.info({ configuration }, 'Configuration from MongoDB'); + const globalConfiguration = await getGlobalConfiguration(logger); + logger.info({ globalConfiguration }, 'Configuration from MongoDB'); let common = {}; try { common = { - configuration, + configuration: globalConfiguration, accountInfo: JSON.parse(cacheTrailingTradeCommon['account-info']), exchangeSymbols: JSON.parse(cacheTrailingTradeCommon['exchange-symbols']), publicURL: cacheTrailingTradeCommon['local-tunnel-url'] @@ -54,12 +57,31 @@ const handleLatest = async (logger, ws, _payload) => { } }); + stats.symbols = await Promise.all( + _.map(stats.symbols, async symbol => { + const newSymbol = symbol; + // Set latest global configuration + newSymbol.globalConfiguration = globalConfiguration; + // Retrieve latest symbol configuration + newSymbol.symbolConfiguration = await getConfiguration( + logger, + newSymbol.symbol + ); + // Retrieve latest last buy price + newSymbol.sell.lastBuyPrice = await getLastBuyPrice( + logger, + newSymbol.symbol + ); + return newSymbol; + }) + ); + logger.info( { account: common.accountInfo, publicURL: common.publicURL, stats, - configuration + configuration: globalConfiguration }, 'stats' ); @@ -68,7 +90,7 @@ const handleLatest = async (logger, ws, _payload) => { JSON.stringify({ result: true, type: 'latest', - configuration, + configuration: globalConfiguration, common, stats }) diff --git a/app/websocket/handlers/setting-update.js b/app/websocket/handlers/setting-update.js index b4925e29..a3b978c1 100644 --- a/app/websocket/handlers/setting-update.js +++ b/app/websocket/handlers/setting-update.js @@ -1,10 +1,15 @@ const _ = require('lodash'); const { mongo, cache } = require('../../helpers'); +const { + deleteAllSymbolConfiguration +} = require('../../jobs/trailingTrade/configuration'); const handleSettingUpdate = async (logger, ws, payload) => { logger.info({ payload }, 'Start setting update'); - const { data } = payload; + const { data: newConfiguration } = payload; + + const { action } = newConfiguration; const cachedConfiguration = await mongo.findOne( logger, @@ -20,11 +25,17 @@ const handleSettingUpdate = async (logger, ws, payload) => { return; } - const newConfiguration = { + const mergedConfiguration = { ...cachedConfiguration, - ..._.pick(data, ['symbols', 'supportFIATs', 'candles', 'buy', 'sell']) + ..._.pick(newConfiguration, [ + 'symbols', + 'supportFIATs', + 'candles', + 'buy', + 'sell' + ]) }; - logger.info({ newConfiguration }, 'New configuration'); + logger.info({ mergedConfiguration }, 'New merged configuration'); await mongo.upsertOne( logger, @@ -32,11 +43,16 @@ const handleSettingUpdate = async (logger, ws, payload) => { { key: 'configuration' }, { key: 'configuration', - ...newConfiguration + ...mergedConfiguration } ); await cache.hdel('trailing-trade-common', 'exchange-symbols'); + if (action === 'apply-to-all') { + // In this case delete all symbol configuration + await deleteAllSymbolConfiguration(logger); + } + ws.send( JSON.stringify({ result: true, diff --git a/app/websocket/handlers/symbol-setting-delete.js b/app/websocket/handlers/symbol-setting-delete.js new file mode 100644 index 00000000..3e32e2a4 --- /dev/null +++ b/app/websocket/handlers/symbol-setting-delete.js @@ -0,0 +1,19 @@ +const { + deleteSymbolConfiguration +} = require('../../jobs/trailingTrade/configuration'); + +const handleSymbolSettingDelete = async (logger, ws, payload) => { + logger.info({ payload }, 'Start symbol setting delete'); + + const { data: symbolInfo } = payload; + + const { symbol } = symbolInfo; + + await deleteSymbolConfiguration(logger, symbol); + + ws.send( + JSON.stringify({ result: true, type: 'symbol-setting-delete-result' }) + ); +}; + +module.exports = { handleSymbolSettingDelete }; diff --git a/jest.setup.js b/jest.setup.js index cc218c52..acb21c47 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,6 +1,4 @@ process.env.TZ = 'Australia/Melbourne'; -process.env.RATE_LIMIT_WINDOWS_MS = 60000; -process.env.RATE_LIMIT_MAX = 20; const jestSetup = async () => {}; module.exports = jestSetup; diff --git a/package.json b/package.json index dccf8d40..db577d2a 100644 --- a/package.json +++ b/package.json @@ -90,10 +90,10 @@ ], "coverageThreshold": { "global": { - "branches": 80, - "functions": 80, - "lines": 80, - "statements": 80 + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 } } }, diff --git a/public/index.html b/public/index.html index be0e51a3..9aec00cb 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,11 @@ - + this.requestWebSocket(), 1000); + this.timerID = setInterval(() => this.requestLatest(), 1000); } componentWillUnmount() { diff --git a/public/js/HighlightChange.js b/public/js/HighlightChange.js index 3dc9ef5a..75477335 100644 --- a/public/js/HighlightChange.js +++ b/public/js/HighlightChange.js @@ -6,16 +6,19 @@ class HightlightChange extends React.Component { super(props); this.state = { - changed: false + changed: false, + children: null }; } - componentWillReceiveProps(newProps) { - if (_.isEqual(this.props.children, newProps.children) === false) { - this.setState({ - changed: true - }); + static getDerivedStateFromProps(newProps, state) { + if (_.isEqual(state.children, newProps.children) === false) { + return { + changed: true, + children: newProps.children + }; } + return null; } render() { diff --git a/public/js/SettingIcon.js b/public/js/SettingIcon.js index d14e3413..8cbf2e6d 100644 --- a/public/js/SettingIcon.js +++ b/public/js/SettingIcon.js @@ -5,22 +5,28 @@ class SettingIcon extends React.Component { constructor(props) { super(props); + this.modalToStateMap = { + setting: 'showSettingModal', + confirm: 'showConfirmModal' + }; + this.state = { - showModal: false, + showSettingModal: false, + showConfirmModal: false, configuration: {} }; this.handleModalShow = this.handleModalShow.bind(this); this.handleModalClose = this.handleModalClose.bind(this); - this.handleFormSubmit = this.handleFormSubmit.bind(this); + this.handleFormSubmit = this.handleFormSubmit.bind(this); this.handleInputChange = this.handleInputChange.bind(this); } componentDidUpdate(nextProps) { // Only update configuration, when the modal is closed and different. if ( - this.state.showModal === false && + this.state.showSettingModal === false && _.isEmpty(nextProps.configuration) === false && _.isEqual(nextProps.configuration, this.state.configuration) === false ) { @@ -30,26 +36,29 @@ class SettingIcon extends React.Component { } } - handleFormSubmit(e) { - e.preventDefault(); + handleFormSubmit(extraConfiguration = {}) { console.log( 'handleFormSubmit this.state.configuration ', this.state.configuration ); - this.props.sendWebSocket('setting-update', this.state.configuration); - this.handleModalClose(); + this.handleModalClose('confirm'); + this.handleModalClose('setting'); + this.props.sendWebSocket('setting-update', { + ...this.state.configuration, + ...extraConfiguration + }); } - handleModalShow() { + handleModalShow(modal) { this.setState({ - showModal: true + [this.modalToStateMap[modal]]: true }); } - handleModalClose() { + handleModalClose(modal) { this.setState({ - showModal: false + [this.modalToStateMap[modal]]: false }); } @@ -85,14 +94,14 @@ class SettingIcon extends React.Component { this.handleModalClose('setting)')} size='md'> -
+ Global Settings @@ -367,15 +376,70 @@ class SettingIcon extends React.Component { -
+ + this.handleModalClose('confirm')} + size='md'> + + + ⚠ Save Changes + + + + Warning: You are about to save the global configuration. +
+
+ Do you want to apply the changes for all symbols or just global + configuration? +
+
+ If you choose to apply for all symbols, then customised symbol + configurations will be removed. +
+
+ If you choose to apply the global configuration only, then the + symbols that are different from the global configuration will be + displayed as customised. +
+ + + + + + +
); } diff --git a/public/js/SymbolSettingIcon.js b/public/js/SymbolSettingIcon.js index a126c892..21b7f53c 100644 --- a/public/js/SymbolSettingIcon.js +++ b/public/js/SymbolSettingIcon.js @@ -5,8 +5,14 @@ class SymbolSettingIcon extends React.Component { constructor(props) { super(props); + this.modalToStateMap = { + setting: 'showSettingModal', + confirm: 'showConfirmModal' + }; + this.state = { - showModal: false, + showSettingModal: false, + showConfirmModal: false, symbolConfiguration: {} }; @@ -23,7 +29,7 @@ class SymbolSettingIcon extends React.Component { componentDidUpdate(nextProps) { // Only update symbol configuration, when the modal is closed and different. if ( - this.state.showModal === false && + this.state.showSettingModal === false && _.get(nextProps, 'symbolInfo.symbolConfiguration', null) !== null && _.isEqual( _.get(nextProps, 'symbolInfo.symbolConfiguration', null), @@ -43,24 +49,25 @@ class SymbolSettingIcon extends React.Component { this.state.symbolConfiguration ); + this.handleModalClose('setting'); + // Send with symbolInfo const { symbolInfo } = this.props; const newSymbolInfo = symbolInfo; newSymbolInfo.configuration = this.state.symbolConfiguration; this.props.sendWebSocket('symbol-setting-update', newSymbolInfo); - this.handleModalClose(); } - handleModalShow() { + handleModalShow(modal) { this.setState({ - showModal: true + [this.modalToStateMap[modal]]: true }); } - handleModalClose() { + handleModalClose(modal) { this.setState({ - showModal: false + [this.modalToStateMap[modal]]: false }); } @@ -81,10 +88,12 @@ class SymbolSettingIcon extends React.Component { }); } - resetToGlobalConfiguration(_e) { - this.setState({ - symbolConfiguration: this.props.globalConfiguration - }); + resetToGlobalConfiguration() { + const { symbolInfo } = this.props; + + this.handleModalClose('confirm'); + this.handleModalClose('setting'); + this.props.sendWebSocket('symbol-setting-delete', symbolInfo); } render() { @@ -100,12 +109,12 @@ class SymbolSettingIcon extends React.Component { this.handleModalClose('setting')} size='md'>
@@ -349,14 +358,14 @@ class SymbolSettingIcon extends React.Component { variant='danger' size='sm' type='button' - onClick={this.resetToGlobalConfiguration}> + onClick={() => this.handleModalShow('confirm')}> Reset to Global Setting + + +
); }