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'>
-
+
+ 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'>
+
+ this.handleModalClose('confirm')}
+ size='md'>
+
+
+ ⚠ Reset to Global Setting
+
+
+
+ Warning: You are about to reset the symbol setting to the global
+ setting.
+
+
+ Do you want to delete current symbol setting?
+
+
+
+
+
+
+
);
}