-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: improved queue processing #562
fix: improved queue processing #562
Conversation
Thanks @uhliksk |
Pausing and resuming is neccessary to be sure there is no running job for current symbol while we are processing it. Without it another already queued job can start before we made neccessary changes in database which can cause interference again. |
@@ -11,6 +11,8 @@ const handleSymbolSettingDelete = async (logger, ws, payload) => { | |||
|
|||
const { symbol } = symbolInfo; | |||
|
|||
await queue.hold(logger, symbol); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can refactor this pattern
- queue.hold
- some action
- queue.executeFor
If you agree, I will refactor the code.
Hello @uhliksk What I mean is that we can add every critical job to the queue of the symbol and not only the Let's say that we have the following: BTCUSDT Queue
This is an issue based on your investigation and it is happened because the order is updated before finishing the trailing trade steps. So my proposed solution like this:
The above scenarios based only on the order update but we can do the same for the rest jobs that interfere with Sample code: queues['BTCUSDT'].process('trailingTrade', executeTrailingTrade);
queues['BTCUSDT'].process('orders', processOrderUpdate);
queues['BTCUSDT'].process('whatever', processWhatever); These called named jobs based on the documentation. And it will not affect the queue mechanics to process jobs. The queue will still process them as FIFO https://optimalbits.github.io/bull/ Let me know if you agree with me. If you agree I can help and try to implement it. |
Hello @habibalkhabbaz, yes, this is cleaner solution but much harder to do as all processes currently between hold() and executeFor() have to be moved to separate functions which can be called by queue processor. I can try rework one, for example buy order, this way as a proof of concept and I'll check if there are any issues in real world with that. |
@habibalkhabbaz I found if we put everything into queue, then Currently when events A, B, C and D are received in short period of time it will:
If we put everything into queue then it will run:
Thus I think current holdAndExecute() solution is more effective. |
Hello @uhliksk
I can see multiple manual actions only using override action. So it can be combined in one process function.
Yes exactly but I think this is what we expect, the trailing trade to run after every change or action. Specially in manual actions. @chrisleekr may have opinion on this too. |
Hello @habibalkhabbaz
If we implement this then queue is not needed anymore as there will be no more than 1 job in queue everytime. Then we just need pause/resume mechanism I already made which will allow running only single job at once in sequence. We can completely remove bull queue and just put executeTrailingTrade() instead of queues[].add() directly. I think counting paused[] and resumed[] will have much less cpu overhead than using Bull queue for those single job queues. |
I removed bull queue, renamed
const config = require('config');
const _ = require('lodash');
const { executeTrailingTrade } = require('../index');
let started = {};
let finished = {};
const REDIS_URL = `redis://:${config.get('redis.password')}@${config.get(
'redis.host'
)}:${config.get('redis.port')}/${config.get('redis.db')}`;
const pause = async (funcLogger, symbol) => {
const logger = funcLogger.child({ helper: 'queue' });
// eslint-disable-next-line no-plusplus
const pos = started[symbol]++;
if (pos > finished[symbol]) {
logger.info({ symbol }, `Queue ${symbol} job #${pos} waiting`);
while (pos > finished[symbol]) {
// eslint-disable-next-line no-await-in-loop, no-promise-executor-return
await new Promise(r => setTimeout(r, 10));
}
}
logger.info({ symbol }, `Queue ${symbol} job #${pos} started`);
};
const resume = async (funcLogger, symbol) => {
const logger = funcLogger.child({ helper: 'queue' });
// eslint-disable-next-line no-plusplus
const pos = finished[symbol]++;
if (started[symbol] === finished[symbol]) {
// eslint-disable-next-line no-multi-assign
started[symbol] = finished[symbol] = 0;
}
logger.info({ symbol }, `Queue ${symbol} job #${pos} finished`);
if (started[symbol] === 0) {
logger.info({ symbol }, `Queue ${symbol} finished last`);
}
};
const hold = async (funcLogger, symbol) => {
const logger = funcLogger.child({ helper: 'queue' });
if (!(symbol in started)) {
logger.info({ symbol }, `No queue created for ${symbol}`);
return;
}
await pause(funcLogger, symbol);
};
const init = async (funcLogger, symbols) => {
started = {};
finished = {};
await Promise.all(
_.map(symbols, async symbol => {
started[symbol] = 0;
finished[symbol] = 0;
})
);
};
/**
* Add executeTrailingTrade job to the queue of a symbol
*
* @param {*} funcLogger
* @param {*} symbol
*/
const executeFor = async (funcLogger, symbol, jobData = {}) => {
const logger = funcLogger.child({ helper: 'queue' });
if (!(symbol in started)) {
logger.error({ symbol }, `No queue created for ${symbol}`);
return;
}
await executeTrailingTrade(
logger,
symbol,
_.get(jobData.data, 'correlationId')
);
await resume(funcLogger, symbol);
};
module.exports = {
init,
hold,
executeFor
}; |
The queue as @habibalkhabbaz suggested is very stable, running But more importantly I finally found the root cause of all random issues with orders here: binance-trading-bot/app/cronjob/trailingTradeHelper/order.js Lines 148 to 156 in b12aa5c
When binance will send order event out of order then const updateGridTradeLastOrder = async (logger, symbol, side, newOrder) => {
const lastOrder = await getGridTradeLastOrder(
logger,
symbol,
side.toLowerCase()
);
if (_.isEmpty(lastOrder) === false) {
await saveGridTradeOrder(
logger,
`${symbol}-grid-trade-last-${side}-order`,
newOrder
);
logger.info(`Updated grid trade last ${side} order to cache`);
} else {
logger.error(`Grid trade last ${side} order doesn't exist`);
}
}; |
I haven't made any changes, so if you have changes go ahead. Sorry for not being helpful. |
You don't have to be sorry. Your idea is good and I'll implement it into new solution. |
Good catch! @uhliksk |
Hello @uhliksk I just checked that |
Hello @habibalkhabbaz Thank you for your suggestion. Yes, I'm aware of both usages, but thanks to logging of last trade order mismatch in Now I have rock solid version tested for last 24 hours with thousands of orders made by tight trigger values without a single issue (just a little bit of financial loss because of trading fees were higher than profit 🙂). I'm reworking lint tests now and will commit updated code today or tomorrow for review. |
@habibalkhabbaz @chrisleekr I have this code in // Preprocess
let canExecuteTrailingTrade;
if (parameters.preprocessFn) {
canExecuteTrailingTrade = await parameters.preprocessFn();
logger.info({ symbol }, `Queue ${symbol} job preprocessed`);
}
// Execute (if preprocessed)
if (parameters.execute !== undefined) {
canExecuteTrailingTrade = parameters.execute;
}
if (canExecuteTrailingTrade) {
await executeTrailingTrade(logger, symbol, jobData);
} In const saveOverrideActionFn = async () => {
await saveOverrideAction(
logger,
symbol,
{
action: 'manual-trade',
order,
actionAt: moment().toISOString(),
triggeredBy: 'user'
},
'The manual order received by the bot. Wait for placing the order.'
);
};
queue.execute(
logger,
symbol,
{
start: true,
preprocessFn: saveOverrideActionFn,
execute: true,
finish: true
},
{
correlationId: _.get(logger, 'fields.correlationId', '')
}
); In mockSaveOverrideAction = jest.fn().mockResolvedValue(true);
jest.mock('../../../../cronjob/trailingTradeHelper/common', () => ({
saveOverrideAction: mockSaveOverrideAction
}));
mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true);
jest.mock('../../../../cronjob', async () => ({
executeTrailingTrade: mockExecuteTrailingTrade
})); ... it('triggers saveOverrideAction', () => {
expect(mockSaveOverrideAction).toHaveBeenCalledWith(
loggerMock,
'BTCUSDT',
{
action: 'manual-trade',
order: {
some: 'value'
},
actionAt: expect.any(String),
triggeredBy: 'user'
},
'The manual order received by the bot. Wait for placing the order.'
);
});
it('triggers executeTrailingTrade', () => {
expect(mockExecuteTrailingTrade).toHaveBeenCalledWith(
loggerMock,
'BTCUSDT',
{
correlationId: 'correlationId'
}
);
}); For me it looks OK, but when I try Any idea what's wrong? The code is fully working in production, I just have issues with tests. Can I force commit without lint tests so you can see the whole thing? |
Hi @uhliksk You are doing it right. Only in the test you have to mock |
I tried but then |
@uhliksk let me try. Oh, can you commit the changes? |
@chrisleekr I finally achieved all tests passed. Should I squash all those commits into single one or can I keep this way? PR is ready to review and merge if you don't find any issues. |
@uhliksk wow, nice work!!! I will review tonight and get back to you! |
} | ||
|
||
// Postprocess | ||
if (modifiers.postprocessFn) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
postprocessFn
seems not to be used anywhere. Can remove it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed it, but let me know if it is supposed to be there.
@uhliksk while I was testing in TestNet, I found something weird. Let me line up what I found first. There was an order which is filled for BNBBTC. But it executed three times in a row. And ended up wrong quantity. This is a log file. BNBBTC-2023-01-10-12-01-40.zip And here is the CSV file extracted from the log file using The log shows that while job one is running, job two is also running. Can the job run simultaneously? This is a screenshot of the log showing the last buy price calculation multiple times. As you can see, the final balance becomes 0.03, which should be 0.01. Is this happening to you as well? Or it's caused by my change - 9e7ec09 If you don't have the same issue, I will revert and test again. |
@chrisleekr I have no issues with my code and I'm little bit lost in your refactored one. I think you have that issue because you changed the original logic as |
@chrisleekr Maybe I misunderstood the logic behind your code as it is confusing for me you created two parts with binance-trading-bot/app/cronjob/trailingTradeHelper/queue.js Lines 65 to 73 in 9e7ec09
|
@chrisleekr If you want I can refactor my code a little bit to make it cleaner to understand a logic behind it as I think you missed a point of how |
@uhliksk yes please. 👍 Just note it means ‘queue’ test is not covering all scenarios. Anyway, I am happy you revert my change and refactor the code! Thank you so much for your work! |
That's true if you don't remove any previous test, if you know what I mean. ;) But still you're right. I should cover all possible scenarios in tests and not just what I think is enough to test the logic.
Thank you. I'll implement |
I simplified the logic as I removed the modifier I moved I removed I added more tests into PR is ready for review. I hope you'll like it. |
@uhliksk New change looks very good. I will quickly go through and merge in! |
This is a really good fix. I've learnt a lot from your code @uhliksk Of course, thanks to @habibalkhabbaz for your input as well. You know I always appreciate you. 💯 As I merged to master, it will build a development image; let's run it for 1-2 days, and I will release it. |
@chrisleekr There is a hidden bug need to be fixed before release. I'll prepare PR. This binance-trading-bot/app/server-binance.js Lines 179 to 184 in 98b756e
|
@chrisleekr Don't need to add |
Description
Related Issue
#456, #487, #560, #574, #575, #576
Motivation and Context
It will prevent the bot to be stuck in unexpected states causing unwanted loss in bear market because when bot is stuck it's not even able to sell.
How Has This Been Tested?
I tested with tight buy and sell triggers to make as much events as possible to test queue hold.
Screenshots (if appropriate):