diff --git a/packages/bot-skeleton/src/scratch/accumulators-proposal-handler.js b/packages/bot-skeleton/src/scratch/accumulators-proposal-handler.js new file mode 100644 index 000000000000..bd1829f43e8a --- /dev/null +++ b/packages/bot-skeleton/src/scratch/accumulators-proposal-handler.js @@ -0,0 +1,39 @@ +import { api_base } from '../services/api'; +import DBotStore from './dbot-store'; + +export const DEFAULT_PROPOSAL_REQUEST = { + amount: undefined, + basis: 'stake', + contract_type: 'ACCU', + currency: undefined, + symbol: undefined, + growth_rate: undefined, + proposal: 1, + subscribe: 1, +}; + +export const forgetAccumulatorsProposalRequest = async instance => { + if (instance && !instance.is_bot_running) { + await api_base?.api?.send({ forget_all: 'proposal' }); + instance.subscription_id_for_accumulators = null; + instance.is_proposal_requested_for_accumulators = false; + window.Blockly.accumulators_request = {}; + } +}; + +export const handleProposalRequestForAccumulators = instance => { + const top_parent_block = instance?.getTopParent(); + const market_block = top_parent_block?.getChildByType('trade_definition_market'); + const symbol = market_block?.getFieldValue('SYMBOL_LIST'); + const currency = DBotStore.instance.client.currency; + const growth_rate = instance?.getFieldValue('GROWTHRATE_LIST') || 0.01; + const amount = instance?.childBlocks_?.[0]?.getField('NUM')?.getValue() || 0; + const proposal_request = { + ...DEFAULT_PROPOSAL_REQUEST, + amount, + currency, + symbol, + growth_rate, + }; + window.Blockly.accumulators_request = proposal_request; +}; diff --git a/packages/bot-skeleton/src/scratch/blocks/Binary/Tick Analysis/index.js b/packages/bot-skeleton/src/scratch/blocks/Binary/Tick Analysis/index.js index 089bfcf922c6..49b8562f6636 100755 --- a/packages/bot-skeleton/src/scratch/blocks/Binary/Tick Analysis/index.js +++ b/packages/bot-skeleton/src/scratch/blocks/Binary/Tick Analysis/index.js @@ -8,3 +8,5 @@ import './check_direction'; import './tick_analysis'; import './last_digit'; import './lastDigitList'; +import './stat'; +import './stat_list'; diff --git a/packages/bot-skeleton/src/scratch/blocks/Binary/Tick Analysis/stat.js b/packages/bot-skeleton/src/scratch/blocks/Binary/Tick Analysis/stat.js new file mode 100644 index 000000000000..56ae61e9d42c --- /dev/null +++ b/packages/bot-skeleton/src/scratch/blocks/Binary/Tick Analysis/stat.js @@ -0,0 +1,34 @@ +import { localize } from '@deriv/translations'; +import { modifyContextMenu } from '../../../utils'; + +Blockly.Blocks.stat = { + init() { + this.jsonInit(this.definition()); + }, + definition() { + return { + message0: localize('Current Stat'), + output: 'Number', + outputShape: Blockly.OUTPUT_SHAPE_ROUND, + colour: Blockly.Colours.Base.colour, + colourSecondary: Blockly.Colours.Base.colourSecondary, + colourTertiary: Blockly.Colours.Base.colourTertiary, + tooltip: localize('Returns the Current Stat'), + category: Blockly.Categories.Tick_Analysis, + }; + }, + meta() { + return { + display_name: localize('Current Stat'), + description: localize('This block gives you the Current Stat value.'), + }; + }, + customContextMenu(menu) { + modifyContextMenu(menu); + }, +}; + +Blockly.JavaScript.javascriptGenerator.forBlock.stat = () => [ + 'Bot.getCurrentStat()', + Blockly.JavaScript.javascriptGenerator.ORDER_ATOMIC, +]; diff --git a/packages/bot-skeleton/src/scratch/blocks/Binary/Tick Analysis/stat_list.js b/packages/bot-skeleton/src/scratch/blocks/Binary/Tick Analysis/stat_list.js new file mode 100644 index 000000000000..00e3883f245f --- /dev/null +++ b/packages/bot-skeleton/src/scratch/blocks/Binary/Tick Analysis/stat_list.js @@ -0,0 +1,34 @@ +import { localize } from '@deriv/translations'; +import { modifyContextMenu } from '../../../utils'; + +Blockly.Blocks.stat_list = { + init() { + this.jsonInit(this.definition()); + }, + definition() { + return { + message0: localize('Current stat list'), + output: 'Array', + outputShape: Blockly.OUTPUT_SHAPE_ROUND, + colour: Blockly.Colours.Base.colour, + colourSecondary: Blockly.Colours.Base.colourSecondary, + colourTertiary: Blockly.Colours.Base.colourTertiary, + tooltip: localize('Returns the list of last digits of 1000 recent tick values'), + category: Blockly.Categories.Tick_Analysis, + }; + }, + meta() { + return { + display_name: localize('Current stat list'), + description: localize('This block gives you a list of the cuurent stats of the last 1000 tick values.'), + }; + }, + customContextMenu(menu) { + modifyContextMenu(menu); + }, +}; + +Blockly.JavaScript.javascriptGenerator.forBlock.stat_list = () => [ + 'Bot.getStatList()', + Blockly.JavaScript.javascriptGenerator.ORDER_ATOMIC, +]; diff --git a/packages/bot-skeleton/src/scratch/blocks/Binary/Tools/Time/index.js b/packages/bot-skeleton/src/scratch/blocks/Binary/Tools/Time/index.js index a873333deb24..e32dbd0ea518 100755 --- a/packages/bot-skeleton/src/scratch/blocks/Binary/Tools/Time/index.js +++ b/packages/bot-skeleton/src/scratch/blocks/Binary/Tools/Time/index.js @@ -2,3 +2,4 @@ import './epoch'; import './timeout'; import './todatetime'; import './totimestamp'; +import './tickdelay'; diff --git a/packages/bot-skeleton/src/scratch/blocks/Binary/Tools/Time/tickdelay.js b/packages/bot-skeleton/src/scratch/blocks/Binary/Tools/Time/tickdelay.js new file mode 100644 index 000000000000..4636b58f45b7 --- /dev/null +++ b/packages/bot-skeleton/src/scratch/blocks/Binary/Tools/Time/tickdelay.js @@ -0,0 +1,84 @@ +import { localize } from '@deriv/translations'; +import { modifyContextMenu, evaluateExpression } from '../../../../utils'; +import DBotStore from '../../../../dbot-store'; + +Blockly.Blocks.tick_delay = { + init() { + this.jsonInit(this.definition()); + const { client } = DBotStore.instance; + if (client && client.is_logged_in) { + this.workspace_to_code = Blockly.JavaScript.javascriptGenerator.workspaceToCode(Blockly.derivWorkspace); + } + }, + definition() { + return { + message0: localize('{{ stack_input }} Run after {{ number }} tick(s)', { + stack_input: '%1', + number: '%2', + }), + args0: [ + { + type: 'input_statement', + name: 'TICKDELAYSTACK', + }, + { + type: 'input_value', + name: 'TICKDELAYVALUE', + }, + ], + colour: Blockly.Colours.Base.colour, + colourSecondary: Blockly.Colours.Base.colourSecondary, + colourTertiary: Blockly.Colours.Base.colourTertiary, + previousStatement: null, + nextStatement: null, + tooltip: localize('Run the blocks inside after a given number of ticks'), + category: Blockly.Categories.Time, + }; + }, + meta() { + return { + display_name: localize('Tick Delayed run'), + description: localize( + 'This block delays execution for a given number of ticks. You can place any blocks within this block. The execution of other blocks in your strategy will be paused until the instructions in this block are carried out.' + ), + }; + }, + customContextMenu(menu) { + modifyContextMenu(menu); + }, + getRequiredValueInputs() { + return { + TICKDELAYVALUE: input_value => { + const evaluated_result = evaluateExpression(input_value); + if (evaluated_result === 'invalid_input') { + // this was done to check if any equation or varible assignment is present in the code. + if (this.workspace_to_code && this.workspace_to_code.includes(input_value)) { + return false; + } + this.error_message = localize('Invalid Input {{ input_value }}.', { input_value }); + return true; + } + + if (evaluated_result < 0) { + this.error_message = localize('Values cannot be negative. Provided value: {{ input_value }}.', { + input_value, + }); + return true; + } + }, + }; + }, +}; + +Blockly.JavaScript.javascriptGenerator.forBlock.tick_delay = block => { + const stack = Blockly.JavaScript.javascriptGenerator.statementToCode(block, 'TICKDELAYSTACK'); + const ticks_value = + Blockly.JavaScript.javascriptGenerator.valueToCode( + block, + 'TICKDELAYVALUE', + Blockly.JavaScript.javascriptGenerator.ORDER_ATOMIC + ) || '1'; + + const code = `Bot.getDelayTickValue(${ticks_value})\n${stack}\n`; + return code; +}; diff --git a/packages/bot-skeleton/src/scratch/blocks/Binary/Trade Definition/trade_definition_accumulator.js b/packages/bot-skeleton/src/scratch/blocks/Binary/Trade Definition/trade_definition_accumulator.js index 5bb9aba7b8f3..084bccc21bec 100644 --- a/packages/bot-skeleton/src/scratch/blocks/Binary/Trade Definition/trade_definition_accumulator.js +++ b/packages/bot-skeleton/src/scratch/blocks/Binary/Trade Definition/trade_definition_accumulator.js @@ -4,6 +4,7 @@ import DBotStore from '../../../dbot-store'; import { modifyContextMenu, runGroupedEvents, runIrreversibleEvents } from '../../../utils'; import { config } from '../../../../constants/config'; import ApiHelpers from '../../../../services/api/api-helpers'; +import { handleProposalRequestForAccumulators } from '../../../accumulators-proposal-handler'; Blockly.Blocks.trade_definition_accumulator = { init() { @@ -101,7 +102,7 @@ Blockly.Blocks.trade_definition_accumulator = { if (!this.workspace || Blockly.derivWorkspace.isFlyoutVisible || this.workspace.isDragging()) { return; } - + handleProposalRequestForAccumulators(this); const trade_definition_block = this.workspace .getAllBlocks(true) .find(block => block.type === 'trade_definition'); diff --git a/packages/bot-skeleton/src/scratch/dbot.js b/packages/bot-skeleton/src/scratch/dbot.js index d0cde99066b3..f5f65fe103a8 100644 --- a/packages/bot-skeleton/src/scratch/dbot.js +++ b/packages/bot-skeleton/src/scratch/dbot.js @@ -12,6 +12,7 @@ import DBotStore from './dbot-store'; import { isAllRequiredBlocksEnabled, updateDisabledBlocks, validateErrorOnBlockDelete } from './utils'; import { loadBlockly } from './blockly'; +import { forgetAccumulatorsProposalRequest } from './accumulators-proposal-handler'; class DBot { constructor() { @@ -53,6 +54,8 @@ class DBot { const symbol = market_block.getFieldValue('SYMBOL_LIST'); const category = this.getFieldValue('TRADETYPECAT_LIST'); const trade_type = this.getFieldValue('TRADETYPE_LIST'); + const is_trade_type_accumulator = trade_type === 'accumulator'; + if (!is_trade_type_accumulator) forgetAccumulatorsProposalRequest(that); if (is_symbol_list_change) { contracts_for.getTradeTypeCategories(market, submarket, symbol).then(categories => { @@ -367,6 +370,7 @@ class DBot { this.interpreter = null; this.interpreter = Interpreter(); await this.interpreter.bot.tradeEngine.watchTicks(this.symbol); + forgetAccumulatorsProposalRequest(this); } /** @@ -400,11 +404,10 @@ class DBot { * Disable blocks outside of any main or independent blocks. */ disableStrayBlocks() { - const isMainBlock = block_type => config.mainBlocks.indexOf(block_type) >= 0; const top_blocks = this.workspace.getTopBlocks(); top_blocks.forEach(block => { - if (!isMainBlock() && !block.isIndependentBlock()) { + if (!block.isMainBlock() && !block.isIndependentBlock()) { this.disableBlocksRecursively(block); } }); @@ -416,8 +419,9 @@ class DBot { * Disable blocks and their optional children. */ disableBlocksRecursively(block) { - if (block.nextConnection?.targetConnection) { - this.disableBlocksRecursively(block.nextConnection.targetConnection.sourceBlock_); + block.setDisabled(true); + if (block?.outputConnection?.targetConnection) { + this.disableBlocksRecursively(block?.outputConnection?.sourceBlock_); } } diff --git a/packages/bot-skeleton/src/scratch/utils/index.js b/packages/bot-skeleton/src/scratch/utils/index.js index 564f033216ce..67814f328b43 100644 --- a/packages/bot-skeleton/src/scratch/utils/index.js +++ b/packages/bot-skeleton/src/scratch/utils/index.js @@ -658,14 +658,12 @@ const download_option = { }; export const excludeOptionFromContextMenu = (menu, exclude_items) => { - if (exclude_items && exclude_items.length > 0) { - for (let i = menu.length - 1; i >= 0; i--) { - const menu_text = localize(menu[i].text); - if (exclude_items.includes(menu_text)) { - menu.splice(i, 1); - } else { - menu[i].text = menu_text; - } + for (let i = 0; i <= menu.length - 1; i++) { + const menu_text = localize(menu[i].text); + if (exclude_items.includes(menu_text)) { + menu.splice(i, 1); + } else { + menu[i].text = menu_text; } } }; @@ -706,3 +704,14 @@ export const modifyContextMenu = (menu, add_new_items = []) => { } } }; + +export const evaluateExpression = value => { + if (!value) return 'invalid_input'; + try { + // eslint-disable-next-line no-new-func + const result = new Function(`return ${value.trim()}`)(); + return isNaN(result) ? 'invalid_input' : result; + } catch (e) { + return 'invalid_input'; + } +}; diff --git a/packages/bot-skeleton/src/services/tradeEngine/Interface/TicksInterface.js b/packages/bot-skeleton/src/services/tradeEngine/Interface/TicksInterface.js index 3023b03ff829..bc5ce2b09dd6 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/Interface/TicksInterface.js +++ b/packages/bot-skeleton/src/services/tradeEngine/Interface/TicksInterface.js @@ -1,5 +1,8 @@ const getTicksInterface = tradeEngine => { return { + getDelayTickValue: (...args) => tradeEngine.getDelayTickValue(...args), + getCurrentStat: (...args) => tradeEngine.getCurrentStat(...args), + getStatList: (...args) => tradeEngine.getStatList(...args), getLastTick: (...args) => tradeEngine.getLastTick(...args), getLastDigit: (...args) => tradeEngine.getLastDigit(...args), getTicks: (...args) => tradeEngine.getTicks(...args), diff --git a/packages/bot-skeleton/src/services/tradeEngine/trade/Ticks.js b/packages/bot-skeleton/src/services/tradeEngine/trade/Ticks.js index 60bd95c0d1c5..a414d19d7e7e 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/trade/Ticks.js +++ b/packages/bot-skeleton/src/services/tradeEngine/trade/Ticks.js @@ -4,6 +4,8 @@ import * as constants from './state/constants'; import { getDirection, getLastDigit } from '../utils/helpers'; import { expectPositiveInteger } from '../utils/sanitize'; import { observer as globalObserver } from '../../../utils/observer'; +import { api_base } from '../../api/api-base'; +import debounce from 'lodash.debounce'; let tickListenerKey; @@ -114,4 +116,117 @@ export default Engine => getPipSize() { return this.$scope.ticksService.pipSizes[this.symbol]; } + + async requestAccumulatorStats() { + const subscription_id = this.subscription_id_for_accumulators; + const is_proposal_requested = this.is_proposal_requested_for_accumulators; + const proposal_request = { + ...window.Blockly.accumulators_request, + amount: this?.tradeOptions?.amount, + basis: this?.tradeOptions?.basis, + contract_type: 'ACCU', + currency: this?.tradeOptions?.currency, + growth_rate: this?.tradeOptions?.growth_rate, + proposal: 1, + subscribe: 1, + symbol: this?.tradeOptions?.symbol, + }; + if (!subscription_id && !is_proposal_requested) { + this.is_proposal_requested_for_accumulators = true; + if (proposal_request) { + await api_base?.api?.send(proposal_request); + } + } + } + + async handleOnMessageForAccumulators() { + let ticks_stayed_in_list = []; + return new Promise(resolve => { + const subscription = api_base.api.onMessage().subscribe(({ data }) => { + if (data.msg_type === 'proposal') { + try { + this.subscription_id_for_accumulators = data.subscription.id; + // this was done because we can multile arrays in the respone and the list comes in reverse order + const stat_list = (data.proposal.contract_details.ticks_stayed_in || []).flat().reverse(); + ticks_stayed_in_list = [...stat_list, ...ticks_stayed_in_list]; + if (ticks_stayed_in_list.length > 0) resolve(ticks_stayed_in_list); + } catch (error) { + globalObserver.emit('Unexpected message type or no proposal found:', error); + } + } + }); + api_base.pushSubscription(subscription); + }); + } + + async fetchStatsForAccumulators() { + try { + // request stats for accumulators + const debouncedAccumulatorsRequest = debounce(() => this.requestAccumulatorStats(), 300); + debouncedAccumulatorsRequest(); + // wait for proposal response + const ticks_stayed_in_list = await this.handleOnMessageForAccumulators(); + return ticks_stayed_in_list; + } catch (error) { + globalObserver.emit('Error in subscription promise:', error); + throw error; + } finally { + // forget all proposal subscriptions so we can fetch new stats data on new call + await api_base?.api?.send({ forget_all: 'proposal' }); + this.is_proposal_requested_for_accumulators = false; + this.subscription_id_for_accumulators = null; + } + } + + async getCurrentStat() { + try { + const ticks_stayed_in = await this.fetchStatsForAccumulators(); + return ticks_stayed_in?.[0]; + } catch (error) { + globalObserver.emit('Error fetching current stat:', error); + } + } + + async getStatList() { + try { + const ticks_stayed_in = await this.fetchStatsForAccumulators(); + // we need to send only lastest 100 ticks + return ticks_stayed_in?.slice(0, 100); + } catch (error) { + globalObserver.emit('Error fetching current stat:', error); + } + } + + async getDelayTickValue(tick_value) { + return new Promise((resolve, reject) => { + try { + const ticks = []; + const symbol = this.symbol; + + const resolveAndExit = () => { + this.$scope.ticksService.stopMonitor({ + symbol, + key: '', + }); + resolve(ticks); + ticks.length = 0; + }; + + const watchTicks = tick_list => { + ticks.push(tick_list); + const current_tick = ticks.length; + if (current_tick === tick_value) { + resolveAndExit(); + } + }; + + const delayExecution = tick_list => watchTicks(tick_list); + + if (Number(tick_value) <= 0) resolveAndExit(); + this.$scope.ticksService.monitor({ symbol, callback: delayExecution }); + } catch (error) { + reject(new Error(`Failed to start tick monitoring: ${error.message}`)); + } + }); + } }; diff --git a/packages/bot-skeleton/src/services/tradeEngine/trade/index.js b/packages/bot-skeleton/src/services/tradeEngine/trade/index.js index 03b79283e13d..b90a3004a3ea 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/trade/index.js +++ b/packages/bot-skeleton/src/services/tradeEngine/trade/index.js @@ -72,6 +72,8 @@ export default class TradeEngine extends Balance(Purchase(Sell(OpenContract(Prop contract: {}, proposals: [], }; + this.subscription_id_for_accumulators = null; + this.is_proposal_requested_for_accumulators = false; this.store = createStore(rootReducer, applyMiddleware(thunk)); } diff --git a/packages/bot-web-ui/src/pages/bot-builder/toolbox/toolbox-items.tsx b/packages/bot-web-ui/src/pages/bot-builder/toolbox/toolbox-items.tsx index 106c04fac013..1c32d16142cd 100644 --- a/packages/bot-web-ui/src/pages/bot-builder/toolbox/toolbox-items.tsx +++ b/packages/bot-web-ui/src/pages/bot-builder/toolbox/toolbox-items.tsx @@ -378,6 +378,8 @@ export const ToolboxItems = ReactDomServer.renderToStaticMarkup( + + @@ -472,6 +474,7 @@ export const ToolboxItems = ReactDomServer.renderToStaticMarkup( +