diff --git a/README.md b/README.md index 4047a5f3..1ae70ee3 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,11 @@ [![CodeCov](https://codecov.io/gh/chrisleekr/binance-trading-bot/branch/master/graph/badge.svg)](https://codecov.io/gh/chrisleekr/binance-trading-bot) [![MIT License](https://img.shields.io/github/license/chrisleekr/binance-trading-bot)](https://github.com/chrisleekr/binance-trading-bot/blob/master/LICENSE) -This is a test project. I am just testing my code. **I cannot guarantee whether -you can make money or not.** +This is a test project. I am just testing my code. + +## Warnings + +**I cannot guarantee whether you can make money or not.** **So use it at your own risk! I have no responsibility for any loss or hardship incurred directly or indirectly by using this code.** @@ -29,6 +32,9 @@ is effective than using MACD indicators. BTCUSDT, ETHUSDT. You can add more FIAT symbols like BUSD, AUD from the frontend. However, I didn't test in the live server. So use with your own risk. +- Note that if the coin is worth less than $10, then the bot will remove the + last buy price because Binance does not allow to place an order of less than + $10. - The bot is using MongoDB to provide a persistence database. However, it does not use the latest MongoDB to support Raspberry Pi 32bit. Used MongoDB version is 3.2.20, which is provided by @@ -124,11 +130,11 @@ Or use the frontend to adjust configurations after launching the application. | Frontend Mobile | Setting | | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| ![Screenshot1](https://user-images.githubusercontent.com/5715919/110196331-ea3d9d00-7e97-11eb-8517-c3eaeb0f2698.png) | ![Screenshot2](https://user-images.githubusercontent.com/5715919/110196341-f7f32280-7e97-11eb-9aea-e645f678e185.png) | +| ![Screenshot1](https://user-images.githubusercontent.com/5715919/110298077-421b0600-8048-11eb-9763-94ebc2159745.png) | ![Screenshot2](https://user-images.githubusercontent.com/5715919/110298101-4a734100-8048-11eb-8916-4d4381d3161e.png) | | Frontend Desktop | | ------------------------------------------------------------------------------------------------------------------- | -| ![Screenshot](https://user-images.githubusercontent.com/5715919/110196322-d2feaf80-7e97-11eb-9ee0-a71e7a5c9ed7.png) | +| ![Screenshot](https://user-images.githubusercontent.com/5715919/110298003-2b74af00-8048-11eb-81d4-52a4696b11f4.png) | ### First trade @@ -166,8 +172,17 @@ Or use the frontend to adjust configurations after launching the application. - [x] Override buy/sell configuration per symbol - [x] Support PWA for frontend - now support "Add to Home screen" - [x] Enable/Disable symbols trading, but continue to monitor +- [x] Add max-size for logging +- [x] Execute chaseStopLossLimitOrder on every process +- [x] Support buy trigger percentage - [ ] Apply chase-stop-loss-limit order for buy signal as well - [ ] Override the lowest value in the frontend - [ ] Re-organise configuration structures - [ ] Allow browser notification - [ ] Secure frontend with the password + +## Acknowledgments + +- [@d0x2f](https://github.com/d0x2f) +- [@Maxoos](https://github.com/Maxoos) +- [@OOtta](https://github.com/OOtta) diff --git a/app/jobs/__tests__/simpleStopChaser.test.js b/app/jobs/__tests__/simpleStopChaser.test.js index 040cfea7..e11b486c 100644 --- a/app/jobs/__tests__/simpleStopChaser.test.js +++ b/app/jobs/__tests__/simpleStopChaser.test.js @@ -21,10 +21,18 @@ describe('simpleStopChaser', () => { lastBuyPercentage: 1.06, stopPercentage: 0.97, limitPercentage: 0.96 + }, + buy: { + enabled: true, + triggerPercentage: 1 + }, + sell: { + enabled: true } }; cache.hset = jest.fn().mockResolvedValue(true); + cache.hdel = jest.fn().mockResolvedValue(true); mongo.findOne = jest.fn((_logger, collection, filter) => { if ( collection === 'simple-stop-chaser-common' && @@ -60,13 +68,31 @@ describe('simpleStopChaser', () => { .fn() .mockResolvedValue({ result: true }); + simpleStopChaserHelper.chaseStopLossLimitOrder = jest + .fn() + .mockResolvedValue({ result: true }); + await simpleStopChaserExecute(logger); }); + it('triggers getConfiguration for global configuration', () => { + expect(simpleStopChaserHelper.getConfiguration).toHaveBeenCalledWith( + logger + ); + }); + + it('triggers getConfiguration for symbol configuration', () => { + expect(simpleStopChaserHelper.getConfiguration).toHaveBeenCalledWith( + logger, + 'BTCUSDT' + ); + }); + it('triggers getIndicator', () => { expect(simpleStopChaserHelper.getIndicators).toHaveBeenCalledWith( + logger, 'BTCUSDT', - logger + jobConfig ); }); @@ -79,9 +105,17 @@ describe('simpleStopChaser', () => { }); it('triggers placeBuyOrder', () => { + expect(simpleStopChaserHelper.placeBuyOrder).toHaveBeenCalledWith( + logger, + { some: 'value' }, + jobConfig + ); + }); + + it('triggers chaseStopLossLimitOrder', () => { expect( - simpleStopChaserHelper.placeBuyOrder - ).toHaveBeenCalledWith(logger, { some: 'value' }); + simpleStopChaserHelper.chaseStopLossLimitOrder + ).toHaveBeenCalledWith(logger, { some: 'value' }, jobConfig); }); }); @@ -101,13 +135,18 @@ describe('simpleStopChaser', () => { .fn() .mockResolvedValue({ result: true }); + simpleStopChaserHelper.chaseStopLossLimitOrder = jest + .fn() + .mockResolvedValue({ result: true }); + await simpleStopChaserExecute(logger); }); it('triggers getIndicator', () => { expect(simpleStopChaserHelper.getIndicators).toHaveBeenCalledWith( + logger, 'ETHUSDT', - logger + jobConfig ); }); @@ -122,6 +161,12 @@ describe('simpleStopChaser', () => { it('does not trigger placeBuyOrder', () => { expect(simpleStopChaserHelper.placeBuyOrder).not.toHaveBeenCalled(); }); + + it('triggers chaseStopLossLimitOrder', () => { + expect( + simpleStopChaserHelper.chaseStopLossLimitOrder + ).toHaveBeenCalledWith(logger, { some: 'value' }, jobConfig); + }); }); describe('when tradeaActionResult is hold and there is cached last symbol and it is last symbol at symbols', () => { @@ -151,6 +196,13 @@ describe('simpleStopChaser', () => { expect(simpleStopChaserHelper.placeBuyOrder).not.toHaveBeenCalled(); }); + it('triggers hdel to place-buy-order-result', () => { + expect(cache.hdel).toHaveBeenCalledWith( + 'simple-stop-chaser-symbols', + 'BTCUSDT-place-buy-order-result' + ); + }); + it('caches last processed time and symbol', () => { expect(cache.hset).toHaveBeenCalledWith( 'simple-stop-chaser-common', @@ -162,7 +214,7 @@ describe('simpleStopChaser', () => { it('triggers chaseStopLossLimitOrder', () => { expect( simpleStopChaserHelper.chaseStopLossLimitOrder - ).toHaveBeenCalledWith(logger, { some: 'value' }); + ).toHaveBeenCalledWith(logger, { some: 'value' }, jobConfig); }); }); diff --git a/app/jobs/simpleStopChaser.js b/app/jobs/simpleStopChaser.js index 95d66044..cdbf9246 100644 --- a/app/jobs/simpleStopChaser.js +++ b/app/jobs/simpleStopChaser.js @@ -24,11 +24,11 @@ const determineNextSymbol = async (symbols, logger) => { }; const execute = async logger => { - logger.info('Trade: Simple Stop-Chasing'); + logger.info('Trade: Start simple-stop-chaser process...'); - const simpleStopChaser = await helper.getConfiguration(logger); + const globalConfiguration = await helper.getConfiguration(logger); - const { symbols } = simpleStopChaser; + const { symbols } = globalConfiguration; logger.info({ symbols }, 'Checking symbols...'); @@ -45,10 +45,12 @@ const execute = async logger => { ); try { - // 0. Get exchange symbols - await helper.getExchangeSymbols(symbolLogger); + // 1. Get exchange symbols + await helper.getExchangeSymbols(symbolLogger, globalConfiguration); - // 0. Get account 9info + const symbolConfiguration = await helper.getConfiguration(logger, symbol); + + // 2. Get account info const accountInfo = await helper.getAccountInfo(symbolLogger); cache.hset( 'simple-stop-chaser-common', @@ -56,41 +58,50 @@ const execute = async logger => { JSON.stringify(accountInfo) ); - // 1. Get indicators - const indicators = await helper.getIndicators(symbol, symbolLogger); + // 3. Get indicators + const indicators = await helper.getIndicators( + symbolLogger, + symbol, + symbolConfiguration + ); - // 2. Determine actions + // 4. Determine actions const tradeActionResult = await helper.determineAction( symbolLogger, - indicators + indicators, + symbolConfiguration ); symbolLogger.info({ tradeActionResult }, 'Determined action.'); - // 3. Place order based on lowest value signal - let orderResult = {}; + // 5. Place order based on lowest value signal + let buyOrderResult = {}; if (tradeActionResult.action === 'buy') { - orderResult = await helper.placeBuyOrder(symbolLogger, indicators); - } else if (tradeActionResult.action === 'sell') { - symbolLogger.warn(`Got sell signal, but do nothing. Never lose money.`); - } else { - // Delete cached buy order result + buyOrderResult = await helper.placeBuyOrder( + symbolLogger, + indicators, + symbolConfiguration + ); + } + + // 6. If action is wait, then clean up cache + if (tradeActionResult.action === 'wait') { cache.hdel( 'simple-stop-chaser-symbols', `${symbol}-place-buy-order-result` ); - - // Check stop loss limit order - orderResult = await helper.chaseStopLossLimitOrder( - symbolLogger, - indicators - ); } - if (orderResult.result) { - symbolLogger.info({ orderResult }, 'Finish processing symbol...'); - } else { - symbolLogger.warn({ orderResult }, 'Finish processing symbol...'); - } + // 7. Check stop loss limit order + const soptLossLimitOrderResult = await helper.chaseStopLossLimitOrder( + symbolLogger, + indicators, + symbolConfiguration + ); + + symbolLogger.info( + { buyOrderResult, soptLossLimitOrderResult }, + 'Trade: Finish simple-stop-chaser process...' + ); } catch (e) { symbolLogger.error(e, `${symbol} Execution failed.`); if ( diff --git a/app/jobs/simpleStopChaser/__tests__/helper.test.js b/app/jobs/simpleStopChaser/__tests__/helper.test.js index b4a77c5a..234a9fb7 100644 --- a/app/jobs/simpleStopChaser/__tests__/helper.test.js +++ b/app/jobs/simpleStopChaser/__tests__/helper.test.js @@ -10,6 +10,8 @@ jest.mock('config'); describe('helper', () => { let result; + let symbolConfiguration; + beforeEach(() => { slack.sendMessage = jest.fn().mockResolvedValue(true); }); @@ -37,7 +39,8 @@ describe('helper', () => { return { myConfig: 'value', buy: { - enabled: false + enabled: false, + triggerPercentage: 1.0 }, sell: { enabled: false @@ -54,7 +57,8 @@ describe('helper', () => { expect(result).toStrictEqual({ myConfig: 'value', buy: { - enabled: false + enabled: false, + triggerPercentage: 1.0 }, sell: { enabled: false @@ -84,7 +88,8 @@ describe('helper', () => { expect(result).toStrictEqual({ myConfig: 'value', buy: { - enabled: true + enabled: true, + triggerPercentage: 1.0 }, sell: { enabled: true @@ -211,7 +216,8 @@ describe('helper', () => { return { enabled: true, buy: { - enabled: true + enabled: true, + triggerPercentage: 1.0 }, sell: { enabled: false @@ -222,60 +228,7 @@ describe('helper', () => { }); }); - describe('when cannot find global/symbol configurations', () => { - beforeEach(async () => { - mongo.findOne = jest.fn().mockResolvedValue(undefined); - - result = await simpleStopChaserHelper.getConfiguration( - logger, - 'BTCUSDT' - ); - }); - - it('triggers mongo.findOne for global configuration', () => { - expect(mongo.findOne).toHaveBeenCalledWith( - logger, - 'simple-stop-chaser-common', - { key: 'configuration' } - ); - }); - - it('triggers mongo.findOne for symbol configuration', () => { - expect(mongo.findOne).toHaveBeenCalledWith( - logger, - 'simple-stop-chaser-symbols', - { key: 'BTCUSDT-configuration' } - ); - }); - - it('triggers config.get', () => { - expect(config.get).toHaveBeenCalledWith('jobs.simpleStopChaser'); - }); - - it('triggers mongo.upsertOne for global configuration', () => { - expect(mongo.upsertOne).toHaveBeenCalledWith( - logger, - 'simple-stop-chaser-common', - { key: 'configuration' }, - { - key: 'configuration', - enabled: true, - buy: { enabled: true }, - sell: { enabled: false } - } - ); - }); - - it('returns epxected value', () => { - expect(result).toStrictEqual({ - enabled: true, - buy: { enabled: true }, - sell: { enabled: false } - }); - }); - }); - - describe('when found global configuration, but not symbol configuration', () => { + describe('without symbol', () => { beforeEach(async () => { mongo.findOne = jest.fn((_logger, collection, filter) => { if ( @@ -287,13 +240,19 @@ describe('helper', () => { some: 'value' }; } + if ( + collection === 'simple-stop-chaser-symbols' && + _.isEqual(filter, { key: 'BTCUSDT-configuration' }) + ) { + return { + enabled: true, + some: 'BTCUSDT-value' + }; + } return null; }); - result = await simpleStopChaserHelper.getConfiguration( - logger, - 'BTCUSDT' - ); + result = await simpleStopChaserHelper.getConfiguration(logger); }); it('does not triggers config.get', () => { @@ -308,61 +267,155 @@ describe('helper', () => { expect(result).toStrictEqual({ enabled: true, some: 'value', - buy: { enabled: true }, + buy: { enabled: true, triggerPercentage: 1.0 }, sell: { enabled: true } }); }); }); - describe('when found global/symbol configuration', () => { - beforeEach(async () => { - mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return { - enabled: true, - some: 'value', - buy: { enabled: true }, - sell: { enabled: true } - }; - } + describe('with symbol', () => { + describe('when cannot find global/symbol configurations', () => { + beforeEach(async () => { + mongo.findOne = jest.fn().mockResolvedValue(undefined); - if ( - collection === 'simple-stop-chaser-symbols' && - _.isEqual(filter, { key: 'BTCUSDT-configuration' }) - ) { - return { + result = await simpleStopChaserHelper.getConfiguration( + logger, + 'BTCUSDT' + ); + }); + + it('triggers mongo.findOne for global configuration', () => { + expect(mongo.findOne).toHaveBeenCalledWith( + logger, + 'simple-stop-chaser-common', + { key: 'configuration' } + ); + }); + + it('triggers mongo.findOne for symbol configuration', () => { + expect(mongo.findOne).toHaveBeenCalledWith( + logger, + 'simple-stop-chaser-symbols', + { key: 'BTCUSDT-configuration' } + ); + }); + + it('triggers config.get', () => { + expect(config.get).toHaveBeenCalledWith('jobs.simpleStopChaser'); + }); + + it('triggers mongo.upsertOne for global configuration', () => { + expect(mongo.upsertOne).toHaveBeenCalledWith( + logger, + 'simple-stop-chaser-common', + { key: 'configuration' }, + { + key: 'configuration', enabled: true, - some: 'symbol-value', - buy: { enabled: false }, + buy: { enabled: true, triggerPercentage: 1.0 }, sell: { enabled: false } - }; - } - return null; + } + ); }); - result = await simpleStopChaserHelper.getConfiguration( - logger, - 'BTCUSDT' - ); + it('returns epxected value', () => { + expect(result).toStrictEqual({ + enabled: true, + buy: { enabled: true, triggerPercentage: 1.0 }, + sell: { enabled: false } + }); + }); }); - it('does not triggers config.get', () => { - expect(config.get).not.toHaveBeenCalled(); - }); + describe('when found global configuration, but not symbol configuration', () => { + beforeEach(async () => { + mongo.findOne = jest.fn((_logger, collection, filter) => { + if ( + collection === 'simple-stop-chaser-common' && + _.isEqual(filter, { key: 'configuration' }) + ) { + return { + enabled: true, + some: 'value' + }; + } + return null; + }); - it('does not triggers mongo.upsertOne', () => { - expect(mongo.upsertOne).not.toHaveBeenCalled(); + result = await simpleStopChaserHelper.getConfiguration( + logger, + 'BTCUSDT' + ); + }); + + it('does not triggers config.get', () => { + expect(config.get).not.toHaveBeenCalled(); + }); + + it('does not triggers mongo.upsertOne', () => { + expect(mongo.upsertOne).not.toHaveBeenCalled(); + }); + + it('returns expected value', () => { + expect(result).toStrictEqual({ + enabled: true, + some: 'value', + buy: { enabled: true, triggerPercentage: 1.0 }, + sell: { enabled: true } + }); + }); }); - it('returns expected value', () => { - expect(result).toStrictEqual({ - enabled: true, - some: 'symbol-value', - buy: { enabled: false }, - sell: { enabled: false } + describe('when found global/symbol configuration', () => { + beforeEach(async () => { + mongo.findOne = jest.fn((_logger, collection, filter) => { + if ( + collection === 'simple-stop-chaser-common' && + _.isEqual(filter, { key: 'configuration' }) + ) { + return { + enabled: true, + some: 'value', + buy: { enabled: true, triggerPercentage: 1.0 }, + sell: { enabled: true } + }; + } + + if ( + collection === 'simple-stop-chaser-symbols' && + _.isEqual(filter, { key: 'BTCUSDT-configuration' }) + ) { + return { + enabled: true, + some: 'symbol-value', + buy: { enabled: false, triggerPercentage: 1.01 }, + sell: { enabled: false } + }; + } + return null; + }); + + result = await simpleStopChaserHelper.getConfiguration( + logger, + 'BTCUSDT' + ); + }); + + it('does not triggers config.get', () => { + expect(config.get).not.toHaveBeenCalled(); + }); + + it('does not triggers mongo.upsertOne', () => { + expect(mongo.upsertOne).not.toHaveBeenCalled(); + }); + + it('returns expected value', () => { + expect(result).toStrictEqual({ + enabled: true, + some: 'symbol-value', + buy: { enabled: false, triggerPercentage: 1.01 }, + sell: { enabled: false } + }); }); }); }); @@ -488,7 +541,7 @@ describe('helper', () => { it('returns expected value', () => { expect(result).toStrictEqual({ result: false, - message: 'Balance is not found. Do not place an order.', + message: 'Do not place a buy order as cannot find a balance.', quoteAssetBalance: {}, baseAssetBalance: {} }); @@ -519,8 +572,7 @@ describe('helper', () => { it('returns expected value', () => { expect(result).toStrictEqual({ result: false, - message: - 'The base asset has enough balance to place a stop-loss limit order. Do not place a buy order.', + message: 'Do not place a buy order as enough BTC to sell.', baseAsset: 'BTC', baseAssetTotalBalance: 0.01, currentBalanceInQuoteAsset: 117.6474, @@ -556,8 +608,7 @@ describe('helper', () => { it('returns expected value', () => { expect(result).toStrictEqual({ result: false, - message: - 'Balance is less than the minimum notional. Do not place an order.', + message: 'Do not place a buy order as not enough USDT to buy BTC.', freeBalance: 9 }); }); @@ -665,7 +716,7 @@ describe('helper', () => { it('returns expected value', () => { expect(result).toStrictEqual({ result: false, - message: 'Balance is not found. Do not place an order.', + message: 'Do not place a sell order as cannot find a balance.', baseAssetBalance: {} }); }); @@ -1002,8 +1053,7 @@ describe('helper', () => { it('returns expected result', () => { expect(result).toStrictEqual({ result: false, - message: - 'Order quantity is less or equal to 0. Do not place an order.', + message: 'Do not place an order as quantity is less or equal to 0.', baseAssetPrice: 11756.29, orderQuantity: 0, freeBalance: 0.005 @@ -1067,7 +1117,7 @@ describe('helper', () => { expect(result).toStrictEqual({ result: false, message: - 'Notional value is less than the minimum notional value. Do not place an order.', + 'Notional value is less than the minimum notional value. Do not place a buy order.', orderPrice: 11756.29 }); }); @@ -1290,37 +1340,20 @@ describe('helper', () => { } ] }); - - mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return { - enabled: false, - cronTime: '* * * * * *', - symbols: ['BTCUSDT'], - supportFIATs: ['USDT', 'BUSD'], - candles: { - interval: '4h', - limit: 100 - }, - stopLossLimit: { - lastBuyPercentage: 1.06, - stopPercentage: 0.97, - limitPercentage: 0.96 - } - }; - } - return null; - }); }); describe('when cache is null and have two FIATs in the configuration', () => { beforeEach(async () => { cache.hget = jest.fn(() => null); - result = await simpleStopChaserHelper.getExchangeSymbols(logger); + symbolConfiguration = { + supportFIATs: ['USDT', 'BUSD'] + }; + + result = await simpleStopChaserHelper.getExchangeSymbols( + logger, + symbolConfiguration + ); }); it('triggers exchangeInfo', () => { @@ -1356,30 +1389,12 @@ describe('helper', () => { beforeEach(async () => { cache.hget = jest.fn(() => ''); - mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return { - enabled: false, - cronTime: '* * * * * *', - symbols: ['BTCUSDT'], - candles: { - interval: '4h', - limit: 100 - }, - stopLossLimit: { - lastBuyPercentage: 1.06, - stopPercentage: 0.97, - limitPercentage: 0.96 - } - }; - } - return null; - }); + symbolConfiguration = {}; - result = await simpleStopChaserHelper.getExchangeSymbols(logger); + result = await simpleStopChaserHelper.getExchangeSymbols( + logger, + symbolConfiguration + ); }); it('triggers exchangeInfo', () => { @@ -1405,7 +1420,14 @@ describe('helper', () => { JSON.stringify(['BNBUSDT', 'BTCUSDT', 'BNBUPUSDT']) ); - result = await simpleStopChaserHelper.getExchangeSymbols(logger); + symbolConfiguration = { + supportFIATs: ['USDT', 'BUSD'] + }; + + result = await simpleStopChaserHelper.getExchangeSymbols( + logger, + symbolConfiguration + ); }); it('does not trigger exchangeInfo', () => { @@ -1424,34 +1446,12 @@ describe('helper', () => { const orgExchangeInfo = require('./fixtures/binance-exchange-info.json'); beforeEach(async () => { - mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return { - enabled: false, - cronTime: '* * * * * *', - symbols: ['BTCUSDT'], - candles: { - interval: '4h', - limit: 100 - }, - stopLossLimit: { - lastBuyPercentage: 1.06, - stopPercentage: 0.97, - limitPercentage: 0.96 - }, - buy: { - enabled: true - }, - sell: { - enabled: true - } - }; + symbolConfiguration = { + candles: { + interval: '4h', + limit: 100 } - return null; - }); + }; binance.client.candles = jest.fn().mockResolvedValue(orgCandles); @@ -1459,7 +1459,11 @@ describe('helper', () => { .fn() .mockResolvedValue(orgExchangeInfo); - result = await simpleStopChaserHelper.getIndicators('BTCUSDT', logger); + result = await simpleStopChaserHelper.getIndicators( + logger, + 'BTCUSDT', + symbolConfiguration + ); }); it('returns expected result', () => { @@ -1470,80 +1474,216 @@ describe('helper', () => { describe('determineAction', () => { const orgIndicators = require('./fixtures/helper-get-indicators.json'); - describe('when lowest closed value is less than last closed value', () => { - beforeEach(async () => { - const indicators = _.cloneDeep(orgIndicators); - indicators.lastCandle.close = '15665.0000'; - indicators.lowestClosed = 16000; - result = await simpleStopChaserHelper.determineAction( - logger, - indicators - ); - }); + describe('when lowest price is less than current price', () => { + describe('buy trigger percentage is 1', () => { + beforeEach(async () => { + const indicators = _.cloneDeep(orgIndicators); + indicators.lastCandle.close = '16010.000'; + indicators.lowestClosed = 16000; - it('returns expected result', () => { - expect(result.symbol).toEqual('BTCUSDT'); - expect(result.action).toEqual('buy'); - expect(result.lastCandleClose).toEqual('15665.0000'); - expect(result.lowestClosed).toEqual(16000); - }); - }); + symbolConfiguration = { + buy: { + triggerPercentage: 1.0 + } + }; - describe('when lowest closed value is higher than last closed value', () => { - beforeEach(async () => { - const indicators = _.cloneDeep(orgIndicators); - indicators.lastCandle.close = '16000.0000'; - indicators.lowestClosed = 15665; - result = await simpleStopChaserHelper.determineAction( - logger, - indicators - ); - }); + result = await simpleStopChaserHelper.determineAction( + logger, + indicators, + symbolConfiguration + ); + }); - it('returns expected result', () => { - expect(result.symbol).toEqual('BTCUSDT'); - expect(result.action).toEqual('wait'); - expect(result.lastCandleClose).toEqual('16000.0000'); - expect(result.lowestClosed).toEqual(15665); + it('returns expected result', () => { + expect(result).toEqual({ + symbol: 'BTCUSDT', + action: 'wait', + lastCandleClose: '16010.000', + lowestClosed: 16000, + triggerPrice: 16000, + triggerPercentage: 1, + timeUTC: expect.any(Object) + }); + }); }); - }); - }); - describe('placeBuyOrder', () => { - const orgAccountInfo = require('./fixtures/binance-account-info.json'); - const orgIndicators = require('./fixtures/helper-indicators.json'); - const orgExchangeInfo = require('./fixtures/binance-exchange-info.json'); + describe('buy trigger percentage is 1.01', () => { + beforeEach(async () => { + const indicators = _.cloneDeep(orgIndicators); + indicators.lastCandle.close = '15900.000'; + indicators.lowestClosed = 15821.0; - beforeEach(() => { - mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return { - enabled: false, - cronTime: '* * * * * *', - symbols: ['BTCUSDT'], - candles: { - interval: '4h', - limit: 100 - }, - stopLossLimit: { - lastBuyPercentage: 1.06, - stopPercentage: 0.97, - limitPercentage: 0.96 - }, + symbolConfiguration = { buy: { - enabled: true - }, - sell: { - enabled: true + triggerPercentage: 1.01 } }; - } - return null; + + result = await simpleStopChaserHelper.determineAction( + logger, + indicators, + symbolConfiguration + ); + }); + + it('returns expected result', () => { + expect(result).toEqual({ + symbol: 'BTCUSDT', + action: 'buy', + lastCandleClose: '15900.000', + lowestClosed: 15821, + triggerPrice: 15979.210000000001, + triggerPercentage: 1.01, + timeUTC: expect.any(Object) + }); + }); + }); + + describe('buy trigger percentage is 0.98', () => { + beforeEach(async () => { + const indicators = _.cloneDeep(orgIndicators); + indicators.lastCandle.close = '15700.000'; + indicators.lowestClosed = 15665; + + symbolConfiguration = { + buy: { + triggerPercentage: 0.98 + } + }; + + result = await simpleStopChaserHelper.determineAction( + logger, + indicators, + symbolConfiguration + ); + }); + + it('returns expected result', () => { + expect(result).toEqual({ + symbol: 'BTCUSDT', + action: 'wait', + lastCandleClose: '15700.000', + lowestClosed: 15665, + triggerPrice: 15351.699999999999, + triggerPercentage: 0.98, + timeUTC: expect.any(Object) + }); + }); + }); + }); + + describe('when current price is less than lowest value', () => { + describe('buy trigger percentage is 1', () => { + beforeEach(async () => { + const indicators = _.cloneDeep(orgIndicators); + indicators.lastCandle.close = '15664.0000'; + indicators.lowestClosed = 15665; + + symbolConfiguration = { + buy: { + triggerPercentage: 1.0 + } + }; + + result = await simpleStopChaserHelper.determineAction( + logger, + indicators, + symbolConfiguration + ); + }); + + it('returns expected result', () => { + expect(result).toEqual({ + symbol: 'BTCUSDT', + action: 'buy', + lastCandleClose: '15664.0000', + lowestClosed: 15665, + triggerPrice: 15665, + triggerPercentage: 1, + timeUTC: expect.any(Object) + }); + }); + }); + + describe('buy trigger percentage is 1.01', () => { + beforeEach(async () => { + const indicators = _.cloneDeep(orgIndicators); + indicators.lastCandle.close = '15664.0000'; + indicators.lowestClosed = 15665; + + symbolConfiguration = { + buy: { + triggerPercentage: 1.01 + } + }; + + result = await simpleStopChaserHelper.determineAction( + logger, + indicators, + symbolConfiguration + ); + }); + + it('returns expected result', () => { + expect(result).toEqual({ + symbol: 'BTCUSDT', + action: 'buy', + lastCandleClose: '15664.0000', + lowestClosed: 15665, + triggerPrice: 15821.65, + triggerPercentage: 1.01, + timeUTC: expect.any(Object) + }); + }); }); + describe('buy trigger percentage is 0.98', () => { + beforeEach(async () => { + const indicators = _.cloneDeep(orgIndicators); + indicators.lastCandle.close = '15664.0000'; + indicators.lowestClosed = 15665; + + symbolConfiguration = { + buy: { + triggerPercentage: 0.98 + } + }; + + result = await simpleStopChaserHelper.determineAction( + logger, + indicators, + symbolConfiguration + ); + }); + + it('returns expected result', () => { + expect(result).toEqual({ + symbol: 'BTCUSDT', + action: 'wait', + lastCandleClose: '15664.0000', + lowestClosed: 15665, + triggerPrice: 15351.699999999999, + triggerPercentage: 0.98, + timeUTC: expect.any(Object) + }); + }); + }); + }); + }); + + describe('placeBuyOrder', () => { + const orgAccountInfo = require('./fixtures/binance-account-info.json'); + const orgIndicators = require('./fixtures/helper-indicators.json'); + const orgExchangeInfo = require('./fixtures/binance-exchange-info.json'); + + beforeEach(() => { + symbolConfiguration = { + maxPurchaseAmount: 100, + buy: { + enabled: true + } + }; + mongo.upsertOne = jest.fn().mockResolvedValue(true); binance.client.order = jest.fn().mockResolvedValue(true); @@ -1555,6 +1695,32 @@ describe('helper', () => { .mockResolvedValue(orgExchangeInfo); }); + describe('when trading is disabled', () => { + beforeEach(async () => { + symbolConfiguration = { + maxPurchaseAmount: 100, + buy: { + enabled: false + } + }; + + const indicators = _.cloneDeep(orgIndicators); + + result = await simpleStopChaserHelper.placeBuyOrder( + logger, + indicators, + symbolConfiguration + ); + }); + + it('returns expected result', () => { + expect(result).toStrictEqual({ + result: false, + message: 'Trading for BTCUSDT is disabled. Do not place an order.' + }); + }); + }); + describe('when fail to get buy balance', () => { beforeEach(async () => { const accountInfo = _.cloneDeep(orgAccountInfo); @@ -1564,13 +1730,17 @@ describe('helper', () => { indicators.symbolInfo.quoteAsset = 'UNKNWN'; indicators.symbolInfo.baseAsset = 'UNKNWN'; - result = await simpleStopChaserHelper.placeBuyOrder(logger, indicators); + result = await simpleStopChaserHelper.placeBuyOrder( + logger, + indicators, + symbolConfiguration + ); }); it('returns expected result', () => { expect(result).toStrictEqual({ result: false, - message: 'Balance is not found. Do not place an order.', + message: 'Do not place a buy order as cannot find a balance.', baseAssetBalance: {}, quoteAssetBalance: {} }); @@ -1595,14 +1765,17 @@ describe('helper', () => { binance.client.accountInfo = jest.fn().mockResolvedValue(accountInfo); - result = await simpleStopChaserHelper.placeBuyOrder(logger, indicators); + result = await simpleStopChaserHelper.placeBuyOrder( + logger, + indicators, + symbolConfiguration + ); }); it('returns expected result', () => { expect(result).toStrictEqual({ result: false, - message: - 'Order quantity is less or equal to 0. Do not place an order.', + message: 'Do not place an order as quantity is less or equal to 0.', baseAssetPrice: 11764.74, freeBalance: 10, orderQuantity: 0 @@ -1624,156 +1797,66 @@ describe('helper', () => { }); binance.client.accountInfo = jest.fn().mockResolvedValue(accountInfo); - result = await simpleStopChaserHelper.placeBuyOrder(logger, indicators); + result = await simpleStopChaserHelper.placeBuyOrder( + logger, + indicators, + symbolConfiguration + ); }); it('returns expected result', () => { expect(result).toStrictEqual({ result: false, message: - 'Notional value is less than the minimum notional value. Do not place an order.', + 'Notional value is less than the minimum notional value. Do not place a buy order.', orderPrice: 11764.74 }); }); }); describe('when good to buy order', () => { - describe('when buy is disabled', () => { - beforeEach(async () => { - mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return { - enabled: false, - cronTime: '* * * * * *', - symbols: ['BTCUSDT'], - candles: { - interval: '4h', - limit: 100 - }, - stopLossLimit: { - lastBuyPercentage: 1.06, - stopPercentage: 0.97, - limitPercentage: 0.96 - }, - buy: { - enabled: false - }, - sell: { - enabled: true - } - }; - } - return null; - }); - const indicators = _.cloneDeep(orgIndicators); - - const accountInfo = _.cloneDeep(orgAccountInfo); - accountInfo.balances = _.map(accountInfo.balances, b => { - const balance = b; - if (balance.asset === 'USDT') { - balance.free = '11'; - } - return balance; - }); - binance.client.accountInfo = jest.fn().mockResolvedValue(accountInfo); - - result = await simpleStopChaserHelper.placeBuyOrder( - logger, - indicators - ); - }); - - it('does not triggers binance.client.order', () => { - expect(binance.client.order).not.toHaveBeenCalled(); - }); + beforeEach(async () => { + const indicators = _.cloneDeep(orgIndicators); - it('does not triggers mongo.upsertOne', () => { - expect(mongo.upsertOne).not.toHaveBeenCalled(); + const accountInfo = _.cloneDeep(orgAccountInfo); + accountInfo.balances = _.map(accountInfo.balances, b => { + const balance = b; + if (balance.asset === 'USDT') { + balance.free = '11'; + } + return balance; }); + binance.client.accountInfo = jest.fn().mockResolvedValue(accountInfo); - it('returns expected result', () => { - expect(result).toStrictEqual({ - result: false, - message: - 'Trading for this symbol is disabled. Do not place an order.' - }); - }); + result = await simpleStopChaserHelper.placeBuyOrder( + logger, + indicators, + symbolConfiguration + ); }); - describe('when buy is enabled', () => { - beforeEach(async () => { - mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return { - enabled: false, - cronTime: '* * * * * *', - symbols: ['BTCUSDT'], - candles: { - interval: '4h', - limit: 100 - }, - stopLossLimit: { - lastBuyPercentage: 1.06, - stopPercentage: 0.97, - limitPercentage: 0.96 - }, - buy: { - enabled: true - }, - sell: { - enabled: true - } - }; - } - return null; - }); - const indicators = _.cloneDeep(orgIndicators); - - const accountInfo = _.cloneDeep(orgAccountInfo); - accountInfo.balances = _.map(accountInfo.balances, b => { - const balance = b; - if (balance.asset === 'USDT') { - balance.free = '11'; - } - return balance; - }); - binance.client.accountInfo = jest.fn().mockResolvedValue(accountInfo); - - result = await simpleStopChaserHelper.placeBuyOrder( - logger, - indicators - ); - }); - - it('triggers binance.client.order', () => { - expect(binance.client.order).toHaveBeenCalledWith({ - price: 11764.74, - quantity: 0.000934, - side: 'buy', - symbol: 'BTCUSDT', - timeInForce: 'GTC', - type: 'LIMIT' - }); + it('triggers binance.client.order', () => { + expect(binance.client.order).toHaveBeenCalledWith({ + price: 11764.74, + quantity: 0.000934, + side: 'buy', + symbol: 'BTCUSDT', + timeInForce: 'GTC', + type: 'LIMIT' }); + }); - it('triggers mongo.upsertOne', () => { - expect(mongo.upsertOne).toHaveBeenCalledWith( - logger, - 'simple-stop-chaser-symbols', - { key: 'BTCUSDT-last-buy-price' }, - { key: 'BTCUSDT-last-buy-price', lastBuyPrice: 11764.74 } - ); - }); + it('triggers mongo.upsertOne', () => { + expect(mongo.upsertOne).toHaveBeenCalledWith( + logger, + 'simple-stop-chaser-symbols', + { key: 'BTCUSDT-last-buy-price' }, + { key: 'BTCUSDT-last-buy-price', lastBuyPrice: 11764.74 } + ); + }); - it('returns expected result', () => { - expect(result).toBeTruthy(); - }); + it('returns expected result', () => { + expect(result).toBeTruthy(); }); }); }); @@ -1783,47 +1866,18 @@ describe('helper', () => { const orgIndicators = require('./fixtures/helper-indicators.json'); const orgExchangeInfo = require('./fixtures/binance-exchange-info.json'); - let configuration; - beforeEach(() => { - configuration = { - enabled: false, - cronTime: '* * * * * *', - symbols: ['BTCUSDT'], - candles: { - interval: '4h', - limit: 100 - }, + symbolConfiguration = { stopLossLimit: { lastBuyPercentage: 1.03, stopPercentage: 0.99, limitPercentage: 0.98 }, - buy: { - enabled: true - }, sell: { enabled: true } }; - mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return configuration; - } - - if ( - collection === 'simple-stop-chaser-symbols' && - _.isEqual(filter, { key: 'BTCUSDT-last-buy-price' }) - ) { - return null; - } - return null; - }); - binance.client.cancelOpenOrders = jest.fn().mockResolvedValue(true); binance.client.exchangeInfo = jest @@ -1855,13 +1909,6 @@ describe('helper', () => { describe('when cache value is undefined', () => { beforeEach(async () => { mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return configuration; - } - if ( collection === 'simple-stop-chaser-symbols' && _.isEqual(filter, { key: 'BTCUSDT-last-buy-price' }) @@ -1875,7 +1922,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -1895,13 +1943,6 @@ describe('helper', () => { describe('when cache value is null', () => { beforeEach(async () => { mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return configuration; - } - if ( collection === 'simple-stop-chaser-symbols' && _.isEqual(filter, { key: 'BTCUSDT-last-buy-price' }) @@ -1915,7 +1956,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -1935,13 +1977,6 @@ describe('helper', () => { describe('when cache value is 0', () => { beforeEach(async () => { mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return configuration; - } - if ( collection === 'simple-stop-chaser-symbols' && _.isEqual(filter, { key: 'BTCUSDT-last-buy-price' }) @@ -1955,7 +1990,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -1975,13 +2011,6 @@ describe('helper', () => { describe('when cache value is empty string', () => { beforeEach(async () => { mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return configuration; - } - if ( collection === 'simple-stop-chaser-symbols' && _.isEqual(filter, { key: 'BTCUSDT-last-buy-price' }) @@ -1995,7 +2024,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -2015,13 +2045,6 @@ describe('helper', () => { describe('when cache value is negative value', () => { beforeEach(async () => { mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return configuration; - } - if ( collection === 'simple-stop-chaser-symbols' && _.isEqual(filter, { key: 'BTCUSDT-last-buy-price' }) @@ -2035,7 +2058,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -2069,13 +2093,6 @@ describe('helper', () => { .mockResolvedValue(accountInfo); mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return configuration; - } - if ( collection === 'simple-stop-chaser-symbols' && _.isEqual(filter, { key: 'BTCUSDT-last-buy-price' }) @@ -2089,7 +2106,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -2113,13 +2131,6 @@ describe('helper', () => { beforeEach(async () => { mongo.deleteOne = jest.fn().mockResolvedValue(true); mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return configuration; - } - if ( collection === 'simple-stop-chaser-symbols' && _.isEqual(filter, { key: 'BTCUSDT-last-buy-price' }) @@ -2148,7 +2159,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -2171,14 +2183,18 @@ describe('helper', () => { describe('when there is enough balance', () => { describe('when selling is disabled', () => { beforeEach(async () => { - mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return { ...configuration, sell: { enabled: false } }; + symbolConfiguration = { + stopLossLimit: { + lastBuyPercentage: 1.03, + stopPercentage: 0.99, + limitPercentage: 0.98 + }, + sell: { + enabled: false } + }; + mongo.findOne = jest.fn((_logger, collection, filter) => { if ( collection === 'simple-stop-chaser-symbols' && _.isEqual(filter, { key: 'BTCUSDT-last-buy-price' }) @@ -2204,7 +2220,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -2223,14 +2240,18 @@ describe('helper', () => { describe('when selling is enabled', () => { beforeEach(async () => { - mongo.findOne = jest.fn((_logger, collection, filter) => { - if ( - collection === 'simple-stop-chaser-common' && - _.isEqual(filter, { key: 'configuration' }) - ) { - return { ...configuration, sell: { enabled: true } }; + symbolConfiguration = { + stopLossLimit: { + lastBuyPercentage: 1.03, + stopPercentage: 0.99, + limitPercentage: 0.98 + }, + sell: { + enabled: true } + }; + mongo.findOne = jest.fn((_logger, collection, filter) => { if ( collection === 'simple-stop-chaser-symbols' && _.isEqual(filter, { key: 'BTCUSDT-last-buy-price' }) @@ -2255,7 +2276,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -2313,7 +2335,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -2339,7 +2362,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); @@ -2372,7 +2396,8 @@ describe('helper', () => { result = await simpleStopChaserHelper.chaseStopLossLimitOrder( logger, - indicators + indicators, + symbolConfiguration ); }); diff --git a/app/jobs/simpleStopChaser/helper.js b/app/jobs/simpleStopChaser/helper.js index 15169262..710e0c91 100644 --- a/app/jobs/simpleStopChaser/helper.js +++ b/app/jobs/simpleStopChaser/helper.js @@ -13,8 +13,6 @@ const getGlobalConfiguration = async logger => { return {}; } - logger.info('Found global configuration.'); - // To avoid undefined, little housekeeping here. if (!configValue.buy) { configValue.buy = { @@ -22,6 +20,10 @@ const getGlobalConfiguration = async logger => { }; } + if (!configValue.buy.triggerPercentage) { + configValue.buy.triggerPercentage = 1.0; + } + if (!configValue.sell) { configValue.sell = { enabled: true @@ -50,8 +52,6 @@ const getSymbolConfiguration = async (logger, symbol = null) => { return {}; } - logger.info({ configValue }, 'Found symbol configuration.'); - return configValue; }; @@ -97,7 +97,7 @@ const getConfiguration = async (logger, symbol = null) => { const symbolConfigValue = await getSymbolConfiguration(logger, symbol); // Merge global and symbol configuration - const configValue = { ...globalConfigValue, ...symbolConfigValue }; + const configValue = _.defaultsDeep(symbolConfigValue, globalConfigValue); let simpleStopChaserConfig = {}; if (_.isEmpty(configValue) === false) { @@ -218,7 +218,7 @@ const getBuyBalance = async (logger, indicators, options) => { if (_.isEmpty(quoteAssetBalance) || _.isEmpty(baseAssetBalance)) { return { result: false, - message: 'Balance is not found. Do not place an order.', + message: 'Do not place a buy order as cannot find a balance.', quoteAssetBalance, baseAssetBalance }; @@ -237,8 +237,7 @@ const getBuyBalance = async (logger, indicators, options) => { ) { return { result: false, - message: - 'The base asset has enough balance to place a stop-loss limit order. Do not place a buy order.', + message: `Do not place a buy order as enough ${baseAsset} to sell.`, baseAsset, baseAssetTotalBalance, lastCandleClose, @@ -263,8 +262,7 @@ const getBuyBalance = async (logger, indicators, options) => { if (freeBalance < +symbolInfo.filterMinNotional.minNotional) { return { result: false, - message: - 'Balance is less than the minimum notional. Do not place an order.', + message: `Do not place a buy order as not enough ${quoteAsset} to buy ${baseAsset}.`, freeBalance }; } @@ -304,7 +302,7 @@ const getSellBalance = async ( if (_.isEmpty(baseAssetBalance)) { return { result: false, - message: 'Balance is not found. Do not place an order.', + message: 'Do not place a sell order as cannot find a balance.', baseAssetBalance }; } @@ -395,7 +393,7 @@ const getBuyOrderQuantity = (logger, symbolInfo, balanceInfo, indicators) => { if (orderQuantity <= 0) { return { result: false, - message: 'Order quantity is less or equal to 0. Do not place an order.', + message: 'Do not place an order as quantity is less or equal to 0.', baseAssetPrice, orderQuantity, freeBalance @@ -433,7 +431,7 @@ const getBuyOrderPrice = (logger, symbolInfo, orderQuantityInfo) => { ) { return { result: false, - message: `Notional value is less than the minimum notional value. Do not place an order.`, + message: `Notional value is less than the minimum notional value. Do not place a buy order.`, orderPrice }; } @@ -587,10 +585,9 @@ const getAccountInfo = async logger => { * Get exchange info from Binance * * @param {*} logger + * @param {*} globalConfiguration */ -const getExchangeSymbols = async logger => { - const simpleStopChaserConfig = await getConfiguration(logger); - +const getExchangeSymbols = async (logger, globalConfiguration) => { const cachedExchangeInfo = await cache.hget( 'simple-stop-chaser-common', 'exchange-symbols' @@ -606,7 +603,7 @@ const getExchangeSymbols = async logger => { const exchangeInfo = await binance.client.exchangeInfo(); - let { supportFIATs } = simpleStopChaserConfig; + let { supportFIATs } = globalConfiguration; if (!supportFIATs) { supportFIATs = config.get('jobs.simpleStopChaser.supportFIATs'); } @@ -655,14 +652,19 @@ const flattenCandlesData = candles => { * Get candles from Binance and determine buy signal with lowest price * * @param {*} logger + * @param {*} symbol + * @param {*} symbolConfiguration + * */ -const getIndicators = async (symbol, logger) => { - const simpleStopChaserConfig = await getConfiguration(logger, symbol); +const getIndicators = async (logger, symbol, symbolConfiguration) => { + const { + candles: { interval, limit } + } = symbolConfiguration; const candles = await binance.client.candles({ symbol, - interval: simpleStopChaserConfig.candles.interval, - limit: simpleStopChaserConfig.candles.limit + interval, + limit }); const [lastCandle] = candles.slice(-1); @@ -693,22 +695,40 @@ const getIndicators = async (symbol, logger) => { * * @param {*} logger * @param {*} indicators + * @param {*} symbolConfiguration */ -const determineAction = async (logger, indicators) => { +const determineAction = async (logger, indicators, symbolConfiguration) => { let action = 'wait'; const { symbol, lowestClosed, lastCandle } = indicators; + const { + buy: { triggerPercentage: buyTriggerPercentage } + } = symbolConfiguration; + + const triggerPrice = lowestClosed * buyTriggerPercentage; - if (lastCandle.close <= lowestClosed) { + if (lastCandle.close <= triggerPrice) { action = 'buy'; logger.info( - { symbol, lowestClosed, close: lastCandle.close }, - "Current price is less than the lowest minimum price. Let's buy it." + { + symbol, + lowestClosed, + close: lastCandle.close, + buyTriggerPercentage, + triggerPrice + }, + "Current price is less than the trigger price. Let's buy it." ); } else { logger.warn( - { symbol, lowestClosed, close: lastCandle.close }, - 'Current price is higher than the lowest minimum price. Do not buy.' + { + symbol, + lowestClosed, + close: lastCandle.close, + buyTriggerPercentage, + triggerPrice + }, + 'Current price is higher than the trigger price. Do not buy.' ); } @@ -718,6 +738,8 @@ const determineAction = async (logger, indicators) => { action, lastCandleClose: lastCandle.close, lowestClosed, + triggerPrice, + triggerPercentage: buyTriggerPercentage, timeUTC: moment().utc() }; @@ -736,21 +758,41 @@ const determineAction = async (logger, indicators) => { * * @param {*} logger * @param {*} indicators + * @param {*} symbolConfiguration */ -const placeBuyOrder = async (logger, indicators) => { +const placeBuyOrder = async (logger, indicators, symbolConfiguration) => { logger.info('Start placing buy order'); const { symbol, symbolInfo } = indicators; - const simpleStopChaserConfig = await getConfiguration(logger, symbol); + const { + maxPurchaseAmount, + buy: { enabled: tradingEnabled } + } = symbolConfiguration; let returnValue; - // 1. Cancel all orders - await cancelOpenOrders(logger, symbol); + // 1. Check trading enabled + if (tradingEnabled === false) { + // Trading is disabled. So do not place an order. + returnValue = { + result: false, + message: `Trading for ${symbol} is disabled. Do not place an order.` + }; - // 2. Get balance for trade asset - const { maxPurchaseAmount } = simpleStopChaserConfig; + logger.warn(returnValue); + cache.hset( + 'simple-stop-chaser-symbols', + `${symbol}-place-buy-order-result`, + JSON.stringify(returnValue) + ); + return returnValue; + } + + // 2. Cancel all orders only if trading is enabled. In case there is manual open order. + await cancelOpenOrders(logger, symbol); + + // 3. Get balance for trade asset const balanceInfo = await getBuyBalance(logger, indicators, { maxPurchaseAmount }); @@ -768,7 +810,7 @@ const placeBuyOrder = async (logger, indicators) => { } logger.info({ balanceInfo }, 'getBuyBalance result'); - // 3. Get order quantity + // 4. Get order quantity const orderQuantityInfo = getBuyOrderQuantity( logger, symbolInfo, @@ -788,7 +830,7 @@ const placeBuyOrder = async (logger, indicators) => { } logger.info({ orderQuantityInfo }, 'getBuyOrderQuantity result'); - // 4. Get order price + // 5. Get order price const orderPriceInfo = getBuyOrderPrice( logger, symbolInfo, @@ -806,25 +848,6 @@ const placeBuyOrder = async (logger, indicators) => { } logger.info({ orderPriceInfo }, 'getBuyOrderPrice result'); - // 5. Check trading enabled - if (simpleStopChaserConfig.buy.enabled === false) { - // Trading is disabled. So do not place an order. - - returnValue = { - result: false, - message: 'Trading for this symbol is disabled. Do not place an order.' - }; - - logger.warn(returnValue); - - cache.hset( - 'simple-stop-chaser-symbols', - `${symbol}-place-buy-order-result`, - JSON.stringify(returnValue) - ); - return returnValue; - } - // 6. Place order const orderParams = { symbol, @@ -850,7 +873,7 @@ const placeBuyOrder = async (logger, indicators) => { `${symbol}-place-buy-order-result`, JSON.stringify({ result: true, - message: `Placed buy order at the price of ${orderPriceInfo.orderPrice}.` + message: `Placed a buy order at the price of ${orderPriceInfo.orderPrice}.` }) ); @@ -880,17 +903,23 @@ const placeBuyOrder = async (logger, indicators) => { * * @param {*} logger * @param {*} indicators + * @param {*} symbolConfiguration */ -const chaseStopLossLimitOrder = async (logger, indicators) => { +const chaseStopLossLimitOrder = async ( + logger, + indicators, + symbolConfiguration +) => { logger.info('Start chaseStopLossLimitOrder'); const { symbol, symbolInfo } = indicators; - const simpleStopChaserConfig = await getConfiguration(logger, symbol); - let returnValue; - const { stopLossLimit: stopLossLimitConfig } = simpleStopChaserConfig; + const { + stopLossLimit: stopLossLimitConfig, + sell: { enabled: tradingEnabled } + } = symbolConfiguration; // 1. Get open orders const openOrders = await getOpenOrders(logger, symbol); @@ -1015,7 +1044,7 @@ const chaseStopLossLimitOrder = async (logger, indicators) => { ); // 0. Check trading enabled - if (simpleStopChaserConfig.sell.enabled === false) { + if (tradingEnabled === false) { // Trading is disabled. So do not place an order. returnValue = { result: false, diff --git a/app/websocket/handlers/__tests__/fixtures/latest-simple-stop-chaser-symbols.json b/app/websocket/handlers/__tests__/fixtures/latest-simple-stop-chaser-symbols.json index 94660634..003ae880 100644 --- a/app/websocket/handlers/__tests__/fixtures/latest-simple-stop-chaser-symbols.json +++ b/app/websocket/handlers/__tests__/fixtures/latest-simple-stop-chaser-symbols.json @@ -6,7 +6,7 @@ "ETHUSDT-open-orders": "[]", "ETHUSDT-last-placed-stop-loss-order": "{\"symbol\":\"ETHUSDT\",\"side\":\"sell\",\"type\":\"STOP_LOSS_LIMIT\",\"quantity\":96.38082,\"price\":14406,\"timeInForce\":\"GTC\",\"stopPrice\":14553}", "ETHUSDT-chase-stop-loss-limit-order-sell-signal": "{\"lastBuyPrice\":0,\"lastCandleClose\":11091.39,\"calculatedLastBuyPrice\":0,\"timeUTC\":\"2021-02-04T22:37:37.514Z\"}", - "ETHUSDT-determine-action": "{\"symbol\":\"ETHUSDT\",\"action\":\"buy\",\"lastCandleClose\":11091.39,\"lowestClosed\":1000,\"timeUTC\":\"2021-02-04T22:37:37.346Z\"}", + "ETHUSDT-determine-action": "{\"symbol\":\"ETHUSDT\",\"action\":\"buy\",\"lastCandleClose\":11091.39,\"lowestClosed\":1000,\"triggerPrice\":1000,\"triggerPercentage\":1,\"timeUTC\":\"2021-02-04T22:37:37.346Z\"}", "ETHUSDT-chase-stop-loss-limit-order-open-order-result": "{\"stopPrice\":\"14553.00000000\",\"lastCandleClose\":11091.39,\"limitPercentage\":0.98,\"limitPrice\":10869.562199999998,\"message\":\"Stop price is higher than limit price. Wait.\",\"timeUTC\":\"2021-02-04T22:37:37.514Z\"}", "ETHUSDT-place-buy-order-result": "{\"result\":false,\"message\":\"The base asset has enough balance to place a stop-loss limit order. Do not place a buy order.\",\"baseAsset\":\"BTC\",\"baseAssetTotalBalance\":1,\"lastCandleClose\":47344.33,\"currentBalanceInQuoteAsset\":47344.33,\"minNotional\":\"10.00000000\"}", @@ -19,10 +19,10 @@ "LTCUSDT-last-placed-stop-loss-order": "{\"symbol\":\"LTCUSDT\",\"side\":\"sell\",\"type\":\"STOP_LOSS_LIMIT\",\"quantity\":499.5,\"price\":196,\"timeInForce\":\"GTC\",\"stopPrice\":198}", "LTCUSDT-symbol-info": "{\"symbol\":\"LTCUSDT\",\"status\":\"TRADING\",\"baseAsset\":\"LTC\",\"baseAssetPrecision\":8,\"quoteAsset\":\"USDT\",\"quotePrecision\":8,\"quoteAssetPrecision\":8,\"baseCommissionPrecision\":8,\"quoteCommissionPrecision\":8,\"orderTypes\":[\"LIMIT\",\"LIMIT_MAKER\",\"MARKET\",\"STOP_LOSS_LIMIT\",\"TAKE_PROFIT_LIMIT\"],\"icebergAllowed\":true,\"ocoAllowed\":true,\"quoteOrderQtyMarketAllowed\":true,\"isSpotTradingAllowed\":true,\"isMarginTradingAllowed\":false,\"filters\":[{\"filterType\":\"PRICE_FILTER\",\"minPrice\":\"0.01000000\",\"maxPrice\":\"100000.00000000\",\"tickSize\":\"0.01000000\"},{\"filterType\":\"PERCENT_PRICE\",\"multiplierUp\":\"5\",\"multiplierDown\":\"0.2\",\"avgPriceMins\":1},{\"filterType\":\"LOT_SIZE\",\"minQty\":\"0.00001000\",\"maxQty\":\"9000.00000000\",\"stepSize\":\"0.00001000\"},{\"filterType\":\"MIN_NOTIONAL\",\"minNotional\":\"10.00000000\",\"applyToMarket\":true,\"avgPriceMins\":1},{\"filterType\":\"ICEBERG_PARTS\",\"limit\":10},{\"filterType\":\"MARKET_LOT_SIZE\",\"minQty\":\"0.00000000\",\"maxQty\":\"1000.00000000\",\"stepSize\":\"0.00000000\"},{\"filterType\":\"MAX_NUM_ALGO_ORDERS\",\"maxNumAlgoOrders\":5},{\"filterType\":\"MAX_NUM_ORDERS\",\"maxNumOrders\":200}],\"permissions\":[\"SPOT\"],\"filterLotSize\":{\"filterType\":\"LOT_SIZE\",\"minQty\":\"0.00001000\",\"maxQty\":\"9000.00000000\",\"stepSize\":\"0.00001000\"},\"filterPrice\":{\"filterType\":\"PRICE_FILTER\",\"minPrice\":\"0.01000000\",\"maxPrice\":\"100000.00000000\",\"tickSize\":\"0.01000000\"},\"filterPercent\":{\"filterType\":\"PERCENT_PRICE\",\"multiplierUp\":\"5\",\"multiplierDown\":\"0.2\",\"avgPriceMins\":1},\"filterMinNotional\":{\"filterType\":\"MIN_NOTIONAL\",\"minNotional\":\"10.00000000\",\"applyToMarket\":true,\"avgPriceMins\":1}}", "LTCUSDT-chase-stop-loss-limit-order-sell-signal-result": "{\"result\":true,\"message\":\"Placed stop loss order.\",\"orderParams\":{\"symbol\":\"LTCUSDT\",\"side\":\"sell\",\"type\":\"STOP_LOSS_LIMIT\",\"quantity\":499.5,\"price\":196,\"timeInForce\":\"GTC\",\"stopPrice\":198},\"orderResult\":{\"symbol\":\"LTCUSDT\",\"orderId\":385,\"orderListId\":-1,\"clientOrderId\":\"fSOrGby8F3GMuNcV94Iqye\",\"transactTime\":1612472864244},\"timeUTC\":\"2021-02-04T21:07:43.835Z\"}", - "LTCUSDT-determine-action": "{\"symbol\":\"LTCUSDT\",\"action\":\"wait\",\"lastCandleClose\":200,\"lowestClosed\":130.56,\"timeUTC\":\"2021-02-04T22:37:38.347Z\"}", + "LTCUSDT-determine-action": "{\"symbol\":\"LTCUSDT\",\"action\":\"wait\",\"lastCandleClose\":200,\"lowestClosed\":130.56,\"triggerPrice\":131.8656,\"triggerPercentage\":1.01,\"timeUTC\":\"2021-02-04T22:37:38.347Z\"}", "LTCUSDT-open-orders": "[{\"symbol\":\"LTCUSDT\",\"orderId\":385,\"orderListId\":-1,\"clientOrderId\":\"fSOrGby8F3GMuNcV94Iqye\",\"price\":\"196.00000000\",\"origQty\":\"499.50000000\",\"executedQty\":\"0.00000000\",\"cummulativeQuoteQty\":\"0.00000000\",\"status\":\"NEW\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"SELL\",\"stopPrice\":\"198.00000000\",\"icebergQty\":\"0.00000000\",\"time\":1612472864244,\"updateTime\":1612472864244,\"isWorking\":false,\"origQuoteOrderQty\":\"0.00000000\"}]", - "BNBUSDT-determine-action": "{\"symbol\":\"BNBUSDT\",\"action\":\"wait\",\"lastCandleClose\":55.4394,\"lowestClosed\":23.9916,\"timeUTC\":\"2021-02-04T22:37:35.363Z\"}", + "BNBUSDT-determine-action": "{\"symbol\":\"BNBUSDT\",\"action\":\"wait\",\"lastCandleClose\":55.4394,\"lowestClosed\":23.9916,\"triggerPrice\":23.9916,\"triggerPercentage\":1,\"timeUTC\":\"2021-02-04T22:37:35.363Z\"}", "BNBUSDT-chase-stop-loss-limit-order": "{\"lastBuyPrice\":0,\"lastCandleClose\":55.9862,\"calculatedLastBuyPrice\":0,\"timeUTC\":\"2021-02-04T20:59:40.521Z\"}", "BNBUSDT-chase-stop-loss-limit-order-result": "{\"result\":false,\"message\":\"Order quantity is less or equal than minimum quantity - 0.01000000. Do not place a stop loss limit order.\",\"quantity\":0,\"timeUTC\":\"2021-02-04T21:01:35.680Z\"}", "BNBUSDT-chase-stop-loss-limit-order-sell-signal-result": "{\"lastBuyPrice\":0,\"lastCandleClose\":55.4394,\"calculatedLastBuyPrice\":0,\"result\":false,\"message\":\"Order quantity is less or equal than minimum quantity - 0.01000000. Do not place an order.\",\"quantity\":0,\"timeUTC\":\"2021-02-04T22:37:35.716Z\"}", @@ -37,7 +37,7 @@ "BTCUSDT-last-buy-price": "43300.35", "BTCUSDT-chase-stop-loss-limit-order-result": "{\"result\":true,\"message\":\"Placed stop loss order.\",\"orderParams\":{\"symbol\":\"BTCUSDT\",\"side\":\"sell\",\"type\":\"STOP_LOSS_LIMIT\",\"quantity\":0.897576,\"price\":25546.51,\"timeInForce\":\"GTC\",\"stopPrice\":25807.19},\"orderResult\":{\"symbol\":\"BTCUSDT\",\"orderId\":5786,\"orderListId\":-1,\"clientOrderId\":\"yGwbtRZNbxkhkgZZwzogQS\",\"transactTime\":1612472022593},\"timeUTC\":\"2021-02-04T20:53:42.182Z\"}", "BTCUSDT-open-orders": "[{\"symbol\":\"BTCUSDT\",\"orderId\":5788,\"orderListId\":-1,\"clientOrderId\":\"kQlANzPdM3gNrkZAF1KDI3\",\"price\":\"43351.35000000\",\"origQty\":\"0.89757600\",\"executedQty\":\"0.10345900\",\"cummulativeQuoteQty\":\"4485.08731965\",\"status\":\"PARTIALLY_FILLED\",\"timeInForce\":\"GTC\",\"type\":\"STOP_LOSS_LIMIT\",\"side\":\"SELL\",\"stopPrice\":\"43793.71000000\",\"icebergQty\":\"0.00000000\",\"time\":1612472732255,\"updateTime\":1612477424145,\"isWorking\":true,\"origQuoteOrderQty\":\"0.00000000\"}]", - "BTCUSDT-determine-action": "{\"symbol\":\"BTCUSDT\",\"action\":\"wait\",\"lastCandleClose\":43351.35,\"lowestClosed\":6000,\"timeUTC\":\"2021-02-04T22:37:36.351Z\"}", + "BTCUSDT-determine-action": "{\"symbol\":\"BTCUSDT\",\"action\":\"wait\",\"lastCandleClose\":43351.35,\"lowestClosed\":6000,\"triggerPrice\":6000,\"triggerPercentage\":1,\"timeUTC\":\"2021-02-04T22:37:36.351Z\"}", "BTCUSDT-chase-stop-loss-limit-order-sell-signal-result": "{\"result\":true,\"message\":\"Placed stop loss order.\",\"orderParams\":{\"symbol\":\"BTCUSDT\",\"side\":\"sell\",\"type\":\"STOP_LOSS_LIMIT\",\"quantity\":0.897576,\"price\":43351.35,\"timeInForce\":\"GTC\",\"stopPrice\":43793.71},\"orderResult\":{\"symbol\":\"BTCUSDT\",\"orderId\":5788,\"orderListId\":-1,\"clientOrderId\":\"kQlANzPdM3gNrkZAF1KDI3\",\"transactTime\":1612472732255},\"timeUTC\":\"2021-02-04T21:05:31.847Z\"}", "BTCUSDT-chase-stop-loss-limit-order-open-order-result": "{\"stopPrice\":\"43793.71000000\",\"lastCandleClose\":43351.35,\"limitPercentage\":0.98,\"limitPrice\":42484.323,\"message\":\"Stop price is higher than limit price. Wait.\",\"timeUTC\":\"2021-02-04T22:37:36.531Z\"}", @@ -47,5 +47,5 @@ "ETHUPUSDT-last-placed-order": "{\"symbol\":\"ETHUPUSDT\",\"side\":\"buy\",\"type\":\"LIMIT\",\"quantity\":0.65,\"price\":154.633,\"timeInForce\":\"GTC\",\"timeUTC\":\"2021-02-07T01:53:31.086Z\"}", "ETHUPUSDT-chase-stop-loss-limit-order-sell-signal": "{\"lastBuyPrice\":154.633,\"lastCandleClose\":156.998,\"calculatedLastBuyPrice\":163.91098000000002,\"timeUTC\":\"2021-02-07T01:55:33.639Z\"}", "ETHUPUSDT-chase-stop-loss-limit-order-sell-signal-result": "{\"lastBuyPrice\":0,\"lastCandleClose\":158.037,\"calculatedLastBuyPrice\":0,\"result\":false,\"message\":\"Balance found, but not enough to sell. Delete last buy price.\",\"freeBalance\":0,\"lockedBalance\":0,\"timeUTC\":\"2021-02-07T01:53:19.836Z\"}", - "ETHUPUSDT-determine-action": "{\"symbol\":\"ETHUPUSDT\",\"action\":\"wait\",\"lastCandleClose\":156.998,\"lowestClosed\":154.201,\"timeUTC\":\"2021-02-07T01:55:33.427Z\"}" + "ETHUPUSDT-determine-action": "{\"symbol\":\"ETHUPUSDT\",\"action\":\"wait\",\"lastCandleClose\":156.998,\"lowestClosed\":154.201,\"triggerPrice\":154.201,\"triggerPercentage\":1,\"timeUTC\":\"2021-02-07T01:55:33.427Z\"}" } diff --git a/app/websocket/handlers/__tests__/fixtures/latest-stats.json b/app/websocket/handlers/__tests__/fixtures/latest-stats.json index 5ee00caa..38053768 100644 --- a/app/websocket/handlers/__tests__/fixtures/latest-stats.json +++ b/app/websocket/handlers/__tests__/fixtures/latest-stats.json @@ -4,14 +4,14 @@ "configuration": { "enabled": true, "candles": { "interval": "15m" }, - "buy": { "enabled": true }, + "buy": { "enabled": true, "triggerPercentage": 1 }, "sell": { "enabled": true } }, "common": { "configuration": { "enabled": true, "candles": { "interval": "15m" }, - "buy": { "enabled": true }, + "buy": { "enabled": true, "triggerPercentage": 1 }, "sell": { "enabled": true } }, "accountInfo": { @@ -68,6 +68,8 @@ "action": "buy", "currentPrice": 11091.39, "lowestPrice": 1000, + "triggerPrice": 1000, + "triggerPercentage": 1, "difference": 1009.1389999999999, "processMessage": "The base asset has enough balance to place a stop-loss limit order. Do not place a buy order.", "updatedAt": "2021-02-04T22:37:37.346Z" @@ -116,7 +118,9 @@ "action": "wait", "currentPrice": 200, "lowestPrice": 130.56, - "difference": 53.18627450980391, + "triggerPrice": 131.8656, + "triggerPercentage": 1.01, + "difference": 51.66957872257814, "processMessage": null, "updatedAt": "2021-02-04T22:37:38.347Z" }, @@ -164,6 +168,8 @@ "action": "wait", "currentPrice": 55.4394, "lowestPrice": 23.9916, + "triggerPrice": 23.9916, + "triggerPercentage": 1, "difference": 131.07837743210123, "processMessage": null, "updatedAt": "2021-02-04T22:37:35.363Z" @@ -212,6 +218,8 @@ "action": "wait", "currentPrice": 43351.35, "lowestPrice": 6000, + "triggerPrice": 6000, + "triggerPercentage": 1, "difference": 622.5225, "processMessage": null, "updatedAt": "2021-02-04T22:37:36.351Z" @@ -260,6 +268,8 @@ "action": "wait", "currentPrice": 156.998, "lowestPrice": 154.201, + "triggerPrice": 154.201, + "triggerPercentage": 1, "difference": 1.8138663173390634, "processMessage": null, "updatedAt": "2021-02-07T01:55:33.427Z" diff --git a/app/websocket/handlers/latest.js b/app/websocket/handlers/latest.js index 1a4859e1..1f291cf0 100644 --- a/app/websocket/handlers/latest.js +++ b/app/websocket/handlers/latest.js @@ -73,6 +73,8 @@ const handleLatest = async (logger, ws, _payload) => { action: null, currentPrice: null, lowestPrice: null, + triggerPrice: null, + triggerPercentage: null, difference: null, processMessage: null, updatedAt: null @@ -133,8 +135,10 @@ const handleLatest = async (logger, ws, _payload) => { finalStat.buy.action = determineAction.action; finalStat.buy.currentPrice = determineAction.lastCandleClose; finalStat.buy.lowestPrice = determineAction.lowestClosed; + finalStat.buy.triggerPrice = determineAction.triggerPrice; + finalStat.buy.triggerPercentage = determineAction.triggerPercentage; finalStat.buy.difference = - (1 - determineAction.lastCandleClose / determineAction.lowestClosed) * + (1 - determineAction.lastCandleClose / determineAction.triggerPrice) * -100; finalStat.buy.updatedAt = determineAction.timeUTC; } diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 524805dc..ed8a6a0c 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -49,13 +49,6 @@ "priceSymbol": "BINANCE_JOBS_ALIVE_PRICE_SYMBOL", "balanceSymbols": "BINANCE_JOBS_ALIVE_BALANCE_SYMBOLS" }, - "cleanup": { - "enabled": { - "__name": "BINANCE_JOBS_CLEANUP_ENABLED", - "__format": "boolean" - }, - "cronTime": "BINANCE_JOBS_CLEANUP_CRONTIME" - }, "simpleStopChaser": { "enabled": { "__name": "BINANCE_JOBS_SIMPLE_STOP_CHASER_ENABLED", @@ -99,6 +92,10 @@ "enabled": { "__name": "BINANCE_JOBS_SIMPLE_STOP_CHASER_BUY_ENABLED", "__format": "boolean" + }, + "triggerPercentage": { + "__name": "BINANCE_JOBS_SIMPLE_STOP_CHASER_BUY_TRIGGER_PERCENTAGE", + "__format": "number" } }, "sell": { diff --git a/config/default.json b/config/default.json index 0b00d4e0..d9b78a46 100644 --- a/config/default.json +++ b/config/default.json @@ -35,10 +35,6 @@ "enabled": true, "cronTime": "0 0 9 * * *" }, - "cleanup": { - "enabled": true, - "cronTime": "0 0 9 * * *" - }, "simpleStopChaser": { "enabled": false, "cronTime": "* * * * * *", @@ -55,7 +51,8 @@ "limitPercentage": 0.96 }, "buy": { - "enabled": true + "enabled": true, + "triggerPercentage": 1.0 }, "sell": { "enabled": true diff --git a/docker-compose.rpi.yml b/docker-compose.rpi.yml index 9f96664d..ad5ad3a0 100644 --- a/docker-compose.rpi.yml +++ b/docker-compose.rpi.yml @@ -28,6 +28,10 @@ services: - REDIS_PASSWORD= ports: - 8080:80 + logging: + driver: 'json-file' + options: + max-size: '50m' binance-redis: container_name: binance-redis diff --git a/docker-compose.server.yml b/docker-compose.server.yml index 9e25d820..a4a6b246 100644 --- a/docker-compose.server.yml +++ b/docker-compose.server.yml @@ -28,6 +28,10 @@ services: - REDIS_PASSWORD= ports: - 8080:80 + logging: + driver: 'json-file' + options: + max-size: '50m' binance-redis: container_name: binance-redis diff --git a/docker-compose.yml b/docker-compose.yml index c874b1a0..2168689e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,10 @@ services: - REDIS_PASSWORD= ports: - 8080:80 + logging: + driver: 'json-file' + options: + max-size: '50m' binance-redis: container_name: binance-redis diff --git a/public/js/App.js b/public/js/App.js index 6cc77e32..878ee733 100644 --- a/public/js/App.js +++ b/public/js/App.js @@ -79,12 +79,10 @@ class App extends React.Component { self.setState({ symbols: _.sortBy(response.stats.symbols, s => { if (s.openOrder.difference) { - return (s.openOrder.difference + 100) * -10; + return (s.openOrder.difference + 300) * -10; } if (s.sell.difference) { - return s.sell.difference > -100 - ? (s.sell.difference + 100) * -10 - : (s.sell.difference + 100) * 10; + return (s.sell.difference + 300) * -10; } return s.buy.difference; }), diff --git a/public/js/CoinWrapper.js b/public/js/CoinWrapper.js index 7f9bd167..1688963b 100644 --- a/public/js/CoinWrapper.js +++ b/public/js/CoinWrapper.js @@ -7,10 +7,10 @@ class CoinWrapper extends React.Component { const className = 'coin-wrapper ' + this.props.extraClassName; - const symbolConfiguration = { - ...configuration, - ...symbolInfo.configuration - }; + const symbolConfiguration = _.defaultsDeep( + symbolInfo.configuration, + configuration + ); return (
100
, then when reach buy price, it will
- only buy 100
worth of the coin.
+ only buy 100
worth of the coin. Note that the bot
+ will remove the last buy price if the coin is less worth than
+ $10.
+ 1.01
and the lowest price is $100
,
+ then the bot will buy the coin when the current price reaches{' '}
+ $101
. You cannot set less than 1
,
+ because it will never reach the trigger price unless there is
+ a deep decline before the next process.
1.06
, minimum profit will be 6%
.
+ Set the minimum profit percentage for selling. i.e. if set{' '}
+ 1.06
, minimum profit will be 6%
. So
+ if the last buy price is $100
, then the bot will
+ sell the coin when the current price reaches $106
+ .
0.99
and current price $106
, stop
price will be $104.94
for stop limit order.
0.98
and current price $106
, limit
price will be $103.88
for stop limit order.
100
, then when reach buy price, it will
- only buy 100
worth of the coin.
+ only buy 100
worth of the coin. Note that the bot
+ will remove the last buy price if the coin is less worth than
+ $10.
+ 1.01
and the lowest price is $100
,
+ then the bot will buy the coin when the current price reaches{' '}
+ $101
. You cannot set less than 1
,
+ because it will never reach the trigger price unless there is
+ a deep decline before the next process.
1.06
, minimum profit will be 6%
.
+ Set the minimum profit percentage for selling. i.e. if set{' '}
+ 1.06
, minimum profit will be 6%
. So
+ if the last buy price is $100
, then the bot will
+ sell the coin when the current price reaches $106
+ .
0.99
and current price $106
, stop
price will be $104.94
for stop limit order.
0.98
and current price $106
, limit
price will be $103.88
for stop limit order.