diff --git a/package-lock.json b/package-lock.json index 0c4927a503..159c491185 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5268,8 +5268,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5293,15 +5292,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5318,22 +5315,19 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5464,8 +5458,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5479,7 +5472,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5496,7 +5488,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5505,15 +5496,13 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5534,7 +5523,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5623,8 +5611,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5638,7 +5625,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5734,8 +5720,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5777,7 +5762,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5799,7 +5783,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5848,15 +5831,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true, - "optional": true + "dev": true } } }, @@ -8624,7 +8605,7 @@ "dev": true, "requires": { "acorn": "^4.0.11", - "clone": "github:aminmarashi/clone#d97b4f" + "clone": "github:aminmarashi/clone#d97b4f0ff3d3afebcaaf4a2ecc9c50fbce914900" }, "dependencies": { "acorn": { diff --git a/src/botPage/bot/TradeEngine/Proposal.js b/src/botPage/bot/TradeEngine/Proposal.js index 01334e4a4d..05a0fa257e 100644 --- a/src/botPage/bot/TradeEngine/Proposal.js +++ b/src/botPage/bot/TradeEngine/Proposal.js @@ -21,7 +21,11 @@ export default Engine => this.data.get('proposals').forEach(proposal => { if (proposal.contractType === contractType) { - toBuy = proposal; + if (proposal.error) { + throw Error(proposal.error.error.error.message); + } else { + toBuy = proposal; + } } }); @@ -42,19 +46,33 @@ export default Engine => this.store.dispatch(clearProposals()); } requestProposals() { - Promise.all( - this.proposalTemplates.map(proposal => - doUntilDone(() => - this.api.subscribeToPriceForContractProposal({ + this.proposalTemplates.map(proposal => + doUntilDone(() => + this.api + .subscribeToPriceForContractProposal({ ...proposal, passthrough: { contractType: proposal.contract_type, uuid : getUUID(), }, }) - ) + .catch(e => { + if (e.error.error.code === 'ContractBuyValidationError') { + const { uuid } = e.error.echo_req.passthrough; + + if (!this.data.hasIn(['forgetProposals', uuid])) { + this.data = this.data.setIn(['proposals', uuid], { + ...proposal, + contractType: proposal.contract_type, + error : e, + }); + } + } else { + this.$scope.observer.emit('Error', e); + } + }) ) - ).catch(e => this.$scope.observer.emit('Error', e)); + ); } observeProposals() { this.listen('proposal', r => { @@ -84,12 +102,15 @@ export default Engine => return Promise.all( proposals.map(proposal => { const { uuid: id } = proposal; + const removeProposal = uuid => { + this.data = this.data.deleteIn(['forgetProposals', uuid]); + }; - this.data = this.data.setIn(['forgetProposals', id], true); - - return doUntilDone(() => this.api.unsubscribeByID(proposal.id)).then(() => { - this.data = this.data.deleteIn(['forgetProposals', id]); - }); + if (proposal.error) { + removeProposal(id); + return Promise.resolve(); + } + return doUntilDone(() => this.api.unsubscribeByID(proposal.id)).then(() => removeProposal(id)); }) ); } diff --git a/src/botPage/bot/tools.js b/src/botPage/bot/tools.js index c55a722e72..2fbbcf82bf 100644 --- a/src/botPage/bot/tools.js +++ b/src/botPage/bot/tools.js @@ -5,32 +5,30 @@ import { notify } from './broadcast'; export const noop = () => {}; -const castBarrierToString = barrier => (barrier > 0 ? `+${barrier}` : `${barrier}`); - export const tradeOptionToProposal = tradeOption => - tradeOption.contractTypes.map(type => ({ - duration_unit: tradeOption.duration_unit, - basis : 'stake', - currency : tradeOption.currency, - symbol : tradeOption.symbol, - duration : tradeOption.duration, - amount : roundBalance({ currency: tradeOption.currency, balance: tradeOption.amount }), - contract_type: type, - ...(tradeOption.prediction !== undefined && { - selected_tick: tradeOption.prediction, - }), - ...(type !== 'TICKLOW' && - type !== 'TICKHIGH' && - tradeOption.prediction !== undefined && { - barrier: tradeOption.prediction, - }), - ...(tradeOption.barrierOffset !== undefined && { - barrier: castBarrierToString(tradeOption.barrierOffset), - }), - ...(tradeOption.secondBarrierOffset !== undefined && { - barrier2: castBarrierToString(tradeOption.secondBarrierOffset), - }), - })); + tradeOption.contractTypes.map(type => { + const proposal = { + duration_unit: tradeOption.duration_unit, + basis : 'stake', + currency : tradeOption.currency, + symbol : tradeOption.symbol, + duration : tradeOption.duration, + amount : roundBalance({ currency: tradeOption.currency, balance: tradeOption.amount }), + contract_type: type, + }; + if (tradeOption.prediction !== undefined) { + proposal.selected_tick = tradeOption.prediction; + } + if (!['TICKLOW', 'TICKHIGH'].includes(type) && tradeOption.prediction !== undefined) { + proposal.barrier = tradeOption.prediction; + } else if (tradeOption.barrierOffset !== undefined) { + proposal.barrier = tradeOption.barrierOffset; + } + if (tradeOption.secondBarrierOffset !== undefined) { + proposal.barrier2 = tradeOption.secondBarrierOffset; + } + return proposal; + }); export const getDirection = ticks => { const { length } = ticks; diff --git a/src/botPage/common/const.js b/src/botPage/common/const.js index d188ef7999..bd4b246509 100644 --- a/src/botPage/common/const.js +++ b/src/botPage/common/const.js @@ -142,7 +142,7 @@ const config = { }, ], }, - barrierTypes: [['+', '+'], ['-', '-']], + barrierTypes: [['Offset +', '+'], ['Offset -', '-']], ohlcFields : [ [translate('Open'), 'open'], [translate('High'), 'high'], @@ -165,45 +165,8 @@ const config = { [translate('8 hours'), '28800'], [translate('1 day'), '86400'], ], - mainBlocks : ['trade', 'before_purchase', 'after_purchase', 'during_purchase'], - durationTypes: { - RISEFALL: [ - [translate('Ticks'), 't'], - [translate('Seconds'), 's'], - [translate('Minutes'), 'm'], - [translate('Hours'), 'h'], - ], - RISEFALLEQUALS: [ - [translate('Ticks'), 't'], - [translate('Seconds'), 's'], - [translate('Minutes'), 'm'], - [translate('Hours'), 'h'], - ], - HIGHERLOWER: [ - [translate('Ticks'), 't'], - [translate('Seconds'), 's'], - [translate('Minutes'), 'm'], - [translate('Hours'), 'h'], - ], - RESET: [ - [translate('Ticks'), 't'], - [translate('Seconds'), 's'], - [translate('Minutes'), 'm'], - [translate('Hours'), 'h'], - ], - TOUCHNOTOUCH : [[translate('Ticks'), 't'], [translate('Minutes'), 'm'], [translate('Hours'), 'h']], - ENDSINOUT : [[translate('Minutes'), 'm'], [translate('Hours'), 'h']], - STAYSINOUT : [[translate('Minutes'), 'm'], [translate('Hours'), 'h']], - ASIANS : [[translate('Ticks'), 't']], - MATCHESDIFFERS: [[translate('Ticks'), 't']], - EVENODD : [[translate('Ticks'), 't']], - OVERUNDER : [[translate('Ticks'), 't']], - HIGHLOWTICKS : [[translate('Ticks'), 't']], - }, - hasPrediction : ['MATCHESDIFFERS', 'OVERUNDER', 'HIGHLOWTICKS'], - hasBarrierOffset : ['HIGHERLOWER', 'TOUCHNOTOUCH', 'ENDSINOUT', 'STAYSINOUT'], - hasSecondBarrierOffset: ['ENDSINOUT', 'STAYSINOUT'], - conditionsCategory : { + mainBlocks : ['trade', 'before_purchase', 'after_purchase', 'during_purchase'], + conditionsCategory: { callput : ['risefall', 'higherlower'], callputequal: ['risefallequals'], touchnotouch: ['touchnotouch'], diff --git a/src/botPage/view/blockly/blocks/shared.js b/src/botPage/view/blockly/blocks/shared.js index 15c2e9ab92..a2ed49c817 100644 --- a/src/botPage/view/blockly/blocks/shared.js +++ b/src/botPage/view/blockly/blocks/shared.js @@ -9,6 +9,7 @@ import { getTokenList, removeAllTokens, } from '../../../../common/utils/storageManager'; +import { observer as globalObserver } from '../../../../common/utils/observer'; let purchaseChoices = [[translate('Click to select'), '']]; @@ -95,7 +96,12 @@ fieldGeneratorMapping.SUBMARKET_LIST = block => () => { return [['', 'Invalid']]; } const submarkets = getActiveSubMarket(markets[marketName].submarkets); - return Object.keys(submarkets).map(e => [submarkets[e].name, e]); + return ( + Object.keys(submarkets) + .map(e => [submarkets[e].name, e]) + // Filter out markets we don't have contracts for + .filter(submarket => !['energy'].includes(submarket[1])) + ); }; fieldGeneratorMapping.SYMBOL_LIST = block => () => { @@ -107,7 +113,12 @@ fieldGeneratorMapping.SYMBOL_LIST = block => () => { const marketName = block.getFieldValue('MARKET_LIST'); const submarkets = getActiveSubMarket(markets[marketName].submarkets); const symbols = getActiveSymbols(submarkets[submarketName].symbols); - return Object.keys(symbols).map(e => [symbols[e].display, symbols[e].symbol]); + return ( + Object.keys(symbols) + .map(e => [symbols[e].display, symbols[e].symbol]) + // Filter out symbols we don't have contracts for (these symbols have only forward-starting) + .filter(symbol => !['frxGBPNOK', 'frxUSDNOK', 'frxUSDNEK', 'frxUSDSEK'].includes(symbol[1])) + ); }; fieldGeneratorMapping.TRADETYPECAT_LIST = block => () => { @@ -126,10 +137,14 @@ fieldGeneratorMapping.TRADETYPE_LIST = block => () => { if (!tradeTypeCat) { return [['', '']]; } - return config.conditionsCategory[tradeTypeCat].map(e => [ - config.opposites[e.toUpperCase()].map(c => c[Object.keys(c)[0]]).join('/'), - e, - ]); + return ( + config.conditionsCategory[tradeTypeCat] + .map(e => [config.opposites[e.toUpperCase()].map(c => c[Object.keys(c)[0]]).join('/'), e]) + // Filter out trade types we don't offer + .filter( + tradeType => !(block.getFieldValue('SUBMARKET_LIST') === 'smart_fx' && tradeType[1] === 'higherlower') + ) + ); }; export const dependentFieldMapping = { @@ -139,9 +154,35 @@ export const dependentFieldMapping = { TRADETYPECAT_LIST: 'TRADETYPE_LIST', }; -export const getAvailableDurations = (symbol, selectedContractType) => { - const contractsForStore = JSON.parse(getStorage('contractsForStore') || '[]'); - let tokenList = getTokenList(); +const contractsForStore = JSON.parse(getStorage('contractsForStore') || '[]'); + +const getContractCategory = input => + Object.keys(config.conditionsCategory).find(c => c === input || config.conditionsCategory[c].includes(input)); + +const matchesBarrierCategory = (contract, contractCategory) => { + const conditions = []; + Object.keys(config.barrierCategories).some(barrierCategory => { + if (config.barrierCategories[barrierCategory].includes(contractCategory)) { + conditions.push(contract.barrier_category === barrierCategory); + } + return conditions.length; + }); + // If `barrierCategory` for `contractCategory` not found fallback to all contracts + return !conditions.includes(false); +}; + +const filterContractsByCategory = (contracts, contractCategory, contractType) => { + if (!contracts) return []; + return contracts.filter(contract => { + // We don't offer forward-starting contracts in Binary Bot, remove these + if (contract.start_type === 'forward') { + return false; + } + return contract.contract_category === contractCategory && matchesBarrierCategory(contract, contractType); + }); +}; + +export const getDurationsForContracts = (contractsAvailable, selectedContractType) => { const defaultDurations = [ [translate('Ticks'), 't'], [translate('Seconds'), 's'], @@ -149,23 +190,97 @@ export const getAvailableDurations = (symbol, selectedContractType) => { [translate('Hours'), 'h'], [translate('Days'), 'd'], ]; + const noDurationsAvailable = [{ label: translate('Not available'), unit: 'na', minimum: 0 }]; + if (!contractsAvailable) return noDurationsAvailable; + + const getMinimumAmount = input => input.replace(/\D/g, ''); + const getDurationIndex = input => defaultDurations.findIndex(d => d[1] === input.replace(/\d+/g, '')); + + const offeredDurations = []; + + const contractsForContractCategory = filterContractsByCategory( + contractsAvailable, + getContractCategory(selectedContractType), + selectedContractType + ); + contractsForContractCategory.forEach(c => { + if (!c.min_contract_duration || !c.max_contract_duration) return; + + const startIndex = getDurationIndex(c.min_contract_duration); + const endIndex = getDurationIndex(c.max_contract_duration === '1d' ? '24h' : c.max_contract_duration); - const getContractsForSymbolFromApi = async underlyingSymbol => { - // Refactor this when reducing WS connections - const api = generateLiveApiInstance(); - - // Try to authorize for accurate contracts response - if (tokenList.length) { - try { - await api.authorize(tokenList[0].token); - } catch (e) { - removeAllTokens(); - tokenList = []; + defaultDurations.slice(startIndex, endIndex + 1).forEach((duration, index) => { + if (!offeredDurations.find(offeredDuration => offeredDuration.unit === duration[1])) { + offeredDurations.push({ + label : duration[0], + unit : duration[1], + minimum: index === 0 ? getMinimumAmount(c.min_contract_duration) : 1, + }); } + }); + }); + // If only intraday contracts available remove day-durations + if (contractsForContractCategory.every(c => c.expiry_type === 'intraday')) { + const dayDurationIndex = offeredDurations.findIndex(d => d[1] === 'd'); + if (dayDurationIndex !== -1) { + offeredDurations.splice(dayDurationIndex, 1); } + } + if (!offeredDurations.length) { + return noDurationsAvailable; + } + return ( + offeredDurations + // Maintain order based on duration unit + .sort((a, b) => getDurationIndex(a.unit) - getDurationIndex(b.unit)) + ); +}; +export const haveContractsForSymbol = underlyingSymbol => { + const contractsForSymbol = contractsForStore.find(c => c.symbol === underlyingSymbol); + if (!contractsForSymbol) { + return false; + } + const tokenList = getTokenList(); + const isDifferentAccount = () => tokenList.length && contractsForSymbol.accountName !== tokenList[0].accountName; + if (isDifferentAccount()) { + return false; + } + // Data expired, return cached data, retrieve updated data in background (if not already doing so) + const isExpiredData = () => Math.floor((Date.now() - contractsForSymbol.timestamp) / 1000) > 600; + if (isExpiredData()) { + const event = `contractsLoaded.${underlyingSymbol}`; + if (!globalObserver.isRegistered(event)) { + globalObserver.register(event); + getContractsAvailableForSymbolFromApi(underlyingSymbol); + } + } + return contractsForSymbol; +}; + +export const getContractsAvailableForSymbol = async underlyingSymbol => { + const contractsForSymbol = haveContractsForSymbol(underlyingSymbol); + if (!contractsForSymbol) { + const contractsAvailableForSymbol = await getContractsAvailableForSymbolFromApi(underlyingSymbol); + return Promise.resolve(contractsAvailableForSymbol.available); + } + return Promise.resolve(contractsForSymbol.available); +}; + +export const getContractsAvailableForSymbolFromApi = async underlyingSymbol => { + const api = generateLiveApiInstance(); + let tokenList = getTokenList(); + if (tokenList.length) { + try { + await api.authorize(tokenList[0].token); + } catch (e) { + removeAllTokens(); + tokenList = []; + } + } + const contractsForSymbol = {}; + try { const response = await api.getContractsForSymbol(underlyingSymbol); - const contractsForSymbol = {}; if (response.contracts_for) { Object.assign(contractsForSymbol, { symbol : underlyingSymbol, @@ -185,82 +300,112 @@ export const getAvailableDurations = (symbol, selectedContractType) => { ); contractsForStore.push(contractsForSymbol); setStorage('contractsForStore', JSON.stringify(contractsForStore)); + globalObserver.unregisterAll(`contractsLoaded.${underlyingSymbol}`); + } + } catch (e) { + if (window.trackJs) { + trackJs.addMetadata('getContractsAvailableForSymbolFromApi Error', e.message); } + } + if (typeof api.disconnect === 'function') { api.disconnect(); - return contractsForSymbol; - }; - const getDurationsForContract = contractsForSymbol => { - if (!contractsForSymbol) return defaultDurations; - - // Resolve contract_category (e.g. risefall = callput) - const contractCategory = Object.keys(config.conditionsCategory).find( - c => c === selectedContractType || config.conditionsCategory[c].includes(selectedContractType) - ); + } + return contractsForSymbol; +}; - // Get contracts based on `contract_category` and `barrier_category` - const contractsForContractCategory = contractsForSymbol.filter(c => { - const meetsBarrierConditions = () => { - const conditions = []; - Object.keys(config.barrierCategories).some(barrierCategory => { - if (config.barrierCategories[barrierCategory].includes(selectedContractType)) { - conditions.push(c.barrier_category === barrierCategory); - } - return conditions.length; - }); - // If `barrierCategory` for `selectedContractType` not found fallback to all contracts for durations - return !conditions.includes(false); - }; - // We don't offer forward-starting contracts in Binary Bot, remove these - if (c.start_type === 'forward') { +export const getBarriersForContracts = (contracts, selectedContractType, selectedDuration, selectedBarrierTypes) => { + const barriers = { values: [] }; + const category = getContractCategory(selectedContractType); + const contractsForContractCategory = filterContractsByCategory(contracts, category, selectedContractType); + + const offsetRegex = new RegExp('^[-|+]([0-9]+.[0-9]+)$'); + const isOffset = input => input && offsetRegex.test(input.toString()); + + if (contractsForContractCategory) { + const barrierProps = ['high_barrier', 'low_barrier']; + + selectedBarrierTypes.forEach((barrierType, index) => { + const selectedOffset = ['+', '-'].includes(barrierType); + + // Find barriers based on selected duration & by selected barrier type + // i.e. Hours & days can have different barrier values, offset + absolute sometimes have different values + let contract; + contract = contractsForContractCategory.find(c => { + const durations = getDurationsForContracts([c], selectedContractType); + if (durations.map(duration => duration.unit).includes(selectedDuration)) { + const barrierIsOffset = () => isOffset(c.barrier || c[barrierProps[index]]); + return (selectedOffset && barrierIsOffset()) || (!selectedOffset && !barrierIsOffset()); + } return false; + }); + if (!contract) { + contract = contractsForContractCategory.find( + c => barrierType === 'absolute' && !isOffset(c.barrier || c.high_barrier) + ); } - return c.contract_category === contractCategory && meetsBarrierConditions(); - }); + // Fallback to contract with smallest barriers + if (!contract) { + contract = contractsForContractCategory + .sort((a, b) => { + const c = a.barrier || a.high_barrier; + const d = b.barrier || b.high_barrier; + return parseFloat(c) - parseFloat(d); + }) + .shift(); + } + const barrierlessCategories = ['reset']; + if (contract && !barrierlessCategories.includes(contract.barrier_category)) { + const propName = contract.barriers === 1 ? 'barrier' : barrierProps[index]; + if (contract[propName]) { + const barrierMatch = contract[propName].toString().match(offsetRegex); + barriers.values[index] = barrierMatch ? barrierMatch[1] : contract[propName]; + } - const getDurationIndex = input => defaultDurations.findIndex(d => d[1] === input.replace(/\d+/g, '')); + if (['intraday', 'tick'].includes(contract.expiry_type) && isOffset(contract[propName])) { + barriers.allowBothTypes = true; // Allow both offset + absolute barriers + } else if (barrierType === 'absolute' && !isOffset(contract[propName])) { + barriers.allowAbsoluteType = true; + } - // Generate list of available durations from filtered contracts - const offeredDurations = []; - contractsForContractCategory.forEach(c => { - const startIndex = getDurationIndex(c.min_contract_duration); - const endIndex = getDurationIndex(c.max_contract_duration === '1d' ? '24h' : c.max_contract_duration); - defaultDurations.slice(startIndex, endIndex + 1).forEach(duration => { - if (!offeredDurations.includes(duration)) { - offeredDurations.push(duration); + if (contract.barriers === 1) { + selectedBarrierTypes.splice(index + 1, 1); } - }); - }); - // If only intraday contracts are available, remove day-durations - if (contractsForContractCategory.every(c => c.expiry_type === 'intraday')) { - const dayDurationIndex = offeredDurations.findIndex(d => d[1] === 'd'); - if (dayDurationIndex !== -1) { - offeredDurations.splice(dayDurationIndex, 1); } - } - offeredDurations.sort((a, b) => getDurationIndex(a[1]) - getDurationIndex(b[1])); - return offeredDurations; - }; - - const getFreshContractsFor = () => - new Promise(resolve => { - getContractsForSymbolFromApi(symbol).then(contractsForSymbolFromApi => { - resolve(getDurationsForContract(contractsForSymbolFromApi.available)); - }); }); + if ( + barriers.values.length === 2 && + selectedBarrierTypes.every(val => val === selectedBarrierTypes[0]) && + barriers.values.every(val => val === barriers.values[0]) + ) { + // Set distinct values if equal barrier types have equal values + barriers.values[1] = (barriers.values[0] * 0.95).toFixed(1); + } + } + return barriers; +}; + +export const getPredictionForContracts = (contracts, selectedContractType) => { + const category = getContractCategory(selectedContractType); + const contractsForContractCategory = filterContractsByCategory(contracts, category, selectedContractType); - // Check if we have local data to get durations from - const contractsForSymbol = contractsForStore.find(c => c.symbol === symbol); - if (contractsForSymbol) { - const isDifferentAccount = () => - tokenList.length && contractsForSymbol.accountName !== tokenList[0].accountName; - const isExpiredData = () => Math.floor((Date.now() - contractsForSymbol.timestamp) / 1000) > 600; - if (isDifferentAccount()) { - return getFreshContractsFor(); - } else if (isExpiredData()) { - // Return cached data, update cached data in background - getContractsForSymbolFromApi(symbol); + const contractMapping = {}; + if (category === 'digits') { + contractMapping.matchesdiffers = ['DIGITMATCH', 'DIGITDIFF']; + contractMapping.overunder = ['DIGITOVER', 'DIGITUNDER']; + } else if (category === 'highlowticks') { + contractMapping.highlowticks = ['TICKHIGH', 'TICKLOW']; + } + + const predictionRange = []; + if (contractMapping[selectedContractType]) { + const contract = contractsForContractCategory.find(c => + contractMapping[selectedContractType].includes(c.contract_type) + ); + if (contract && contract.last_digit_range) { + predictionRange.push(...contract.last_digit_range); + } else { + predictionRange.push(0); } - return Promise.resolve(getDurationsForContract(contractsForSymbol.available)); } - return getFreshContractsFor(); + return predictionRange; }; diff --git a/src/botPage/view/blockly/blocks/trade/backwardCompatibility.js b/src/botPage/view/blockly/blocks/trade/backwardCompatibility.js index 89df0052de..0a7715ac9b 100644 --- a/src/botPage/view/blockly/blocks/trade/backwardCompatibility.js +++ b/src/botPage/view/blockly/blocks/trade/backwardCompatibility.js @@ -2,7 +2,7 @@ import { translate } from '../../../../../common/i18n'; import config from '../../../../common/const'; import { symbolApi } from '../../../shared'; import { setInputList, marketDefPlaceHolders, marketToTradeOption } from './tools'; -import { duration, payout, prediction, barrierOffset, secondBarrierOffset } from './components'; +import { duration, payout, prediction, barrierOffsetGenerator } from './components'; const isBlockCreationEvent = (ev, block) => ev.type === Blockly.Events.CREATE && ev.ids.indexOf(block.id) >= 0; @@ -47,8 +47,8 @@ export default () => { duration(this); payout(this); prediction(this); - barrierOffset(this); - secondBarrierOffset(this); + barrierOffsetGenerator('BARRIEROFFSET', this); + barrierOffsetGenerator('SECONDBARRIEROFFSET', this); this.setInputsInline(false); this.setPreviousStatement(true, 'Condition'); }, @@ -63,22 +63,9 @@ export default () => { }); duration(this); payout(this); - if (config.hasPrediction.indexOf(oppositesName) > -1) { - prediction(this); - } else { - this.removeInput('PREDICTION'); - } - if (config.hasBarrierOffset.indexOf(oppositesName) > -1) { - barrierOffset(this); - } else { - this.removeInput('BARRIEROFFSET'); - } - if (config.hasSecondBarrierOffset.indexOf(oppositesName) > -1) { - barrierOffset(this); - secondBarrierOffset(this); - } else { - this.removeInput('SECONDBARRIEROFFSET'); - } + prediction(this); + barrierOffsetGenerator('BARRIEROFFSET', this); + barrierOffsetGenerator('SECONDBARRIEROFFSET', this); this.setInputsInline(false); this.setPreviousStatement(true, 'Condition'); }, diff --git a/src/botPage/view/blockly/blocks/trade/components.js b/src/botPage/view/blockly/blocks/trade/components.js index ad5a139c21..a11467343e 100644 --- a/src/botPage/view/blockly/blocks/trade/components.js +++ b/src/botPage/view/blockly/blocks/trade/components.js @@ -69,62 +69,39 @@ export const payout = block => { } }; -export const barrierOffset = block => { - if (!block.getInput('BARRIEROFFSET')) { - block - .appendValueInput('BARRIEROFFSET') - .setCheck('Number') - .appendField(`${translate('Barrier Offset')} 1:`) - .appendField(new Blockly.FieldDropdown(config.barrierTypes), 'BARRIEROFFSETTYPE_LIST'); - } else { - const barrierOffsetList = block.getField('BARRIEROFFSETTYPE_LIST'); +export const barrierOffsetGenerator = (inputName, block) => { + if (!block.getInput(inputName)) { + // Determine amount of barrierOffset-blocks on workspace + const barrierNumber = block.inputList.filter(input => /BARRIEROFFSET$/.test(input.name)).length; - if ( - !block.workspace.getBlockById('BARRIERVALUE') && - !block.getInput('BARRIEROFFSET').connection.isConnected() - ) { - const barrierValue = block.workspace.newBlock('math_number', 'BARRIERVALUE'); - barrierOffsetList.setValue('+'); - barrierValue.setFieldValue('0.27', 'NUM'); - barrierValue.setShadow(true); - barrierValue.outputConnection.connect(block.getInput('BARRIEROFFSET').connection); - barrierValue.initSvg(); - barrierValue.render(); - } - } -}; + // Set barrier options according to barrierNumber (i.e. Offset + and Offset -) + const barrierOffsetList = new Blockly.FieldDropdown(config.barrierTypes); + barrierOffsetList.prefixField = null; + barrierOffsetList.menuGenerator_ = config.barrierTypes; // eslint-disable-line no-underscore-dangle + barrierOffsetList.setValue(''); + barrierOffsetList.setValue(config.barrierTypes[barrierNumber % config.barrierTypes.length][1]); -export const secondBarrierOffset = block => { - if (!block.getInput('SECONDBARRIEROFFSET')) { block - .appendValueInput('SECONDBARRIEROFFSET') + .appendValueInput(inputName) .setCheck('Number') - .appendField(`${translate('Barrier Offset')} 2:`) - .appendField(new Blockly.FieldDropdown(config.barrierTypes), 'SECONDBARRIEROFFSETTYPE_LIST'); - } else { - const barrierOffsetList = block.getField('SECONDBARRIEROFFSETTYPE_LIST'); + .appendField(`${translate('Barrier')} ${barrierNumber + 1}:`) + .appendField(barrierOffsetList, `${inputName}TYPE_LIST`); - if ( - !block.workspace.getBlockById('SECONDBARRIERVALUE') && - !block.getInput('SECONDBARRIEROFFSET').connection.isConnected() - ) { - const secondBarrierValue = block.workspace.newBlock('math_number', 'SECONDBARRIERVALUE'); - barrierOffsetList.setValue('-'); - secondBarrierValue.setFieldValue('0.27', 'NUM'); - secondBarrierValue.setShadow(true); - secondBarrierValue.outputConnection.connect(block.getInput('SECONDBARRIEROFFSET').connection); - secondBarrierValue.initSvg(); - secondBarrierValue.render(); - } + const input = block.getInput(inputName); + input.setVisible(false); } }; export const prediction = block => { - if (!block.getInput('PREDICTION')) { + const inputName = 'PREDICTION'; + if (!block.getInput(inputName)) { block - .appendValueInput('PREDICTION') + .appendValueInput(inputName) .setCheck('Number') - .appendField(translate('Prediction:')); + .appendField(`${translate('Prediction')}:`); + + const input = block.getInput(inputName); + input.setVisible(false); } }; diff --git a/src/botPage/view/blockly/blocks/trade/tools.js b/src/botPage/view/blockly/blocks/trade/tools.js index 16b20e6258..ad62dbdf0b 100644 --- a/src/botPage/view/blockly/blocks/trade/tools.js +++ b/src/botPage/view/blockly/blocks/trade/tools.js @@ -1,20 +1,14 @@ import { marketDropdown, tradeTypeDropdown, candleInterval, contractTypes, restart } from './components'; import { findTopParentBlock } from '../../utils'; - import { observer as globalObserver } from '../../../../../common/utils/observer'; -export const getTradeType = block => { - const tradeDefBlock = findTopParentBlock(block); - return tradeDefBlock && tradeDefBlock.getFieldValue('TRADETYPE_LIST'); -}; - -export const getSelectedSymbol = block => { - const tradeDefBlock = findTopParentBlock(block); - return tradeDefBlock && tradeDefBlock.getFieldValue('SYMBOL_LIST'); +export const getParentValue = (block, fieldName) => { + const parentBlock = findTopParentBlock(block); + return parentBlock && parentBlock.getFieldValue(fieldName); }; export const updateInputList = block => { - const tradeType = getTradeType(block); + const tradeType = getParentValue(block, 'TRADETYPE_LIST'); if (tradeType) { Blockly.Blocks[tradeType].init.call(block); } diff --git a/src/botPage/view/blockly/blocks/trade/tradeOptions.js b/src/botPage/view/blockly/blocks/trade/tradeOptions.js index 965d5d8fa4..a9a5f44157 100644 --- a/src/botPage/view/blockly/blocks/trade/tradeOptions.js +++ b/src/botPage/view/blockly/blocks/trade/tradeOptions.js @@ -1,9 +1,17 @@ -import { setInputList, updateInputList, getTradeType, getSelectedSymbol } from './tools'; -import { expectValue, getAvailableDurations } from '../shared'; +import { setInputList, updateInputList, getParentValue } from './tools'; +import { + expectValue, + haveContractsForSymbol, + getContractsAvailableForSymbol, + getDurationsForContracts, + getBarriersForContracts, + getPredictionForContracts, +} from '../shared'; import { insideTrade } from '../../relationChecker'; -import { findTopParentBlock } from '../../utils'; -import config from '../../../../common/const'; +import { findTopParentBlock, hideInteractionsFromBlockly, getBlocksByType } from '../../utils'; import { translate } from '../../../../../common/i18n'; +import { observer as globalObserver } from '../../../../../common/utils/observer'; +import config from '../../../../common/const'; export default () => { Blockly.Blocks.tradeOptions = { @@ -17,96 +25,336 @@ export default () => { if (ev.group === 'BackwardCompatibility') { return; } - if (ev.type === Blockly.Events.CREATE || ev.type === Blockly.Events.MOVE) { - Blockly.Events.fire( - new Blockly.Events.Change(this, 'field', 'SYMBOL_LIST', '', this.getFieldValue('SYMBOL_LIST')) - ); - } - if ([Blockly.Events.CREATE, Blockly.Events.CHANGE].includes(ev.type)) { - updateInputList(this); - } - if (ev.name === 'TRADETYPE_LIST') { + if (ev.type === Blockly.Events.CREATE) { updateInputList(this); - } - if (ev.name === 'SYMBOL_LIST' || ev.name === 'TRADETYPE_LIST') { + const block = Blockly.mainWorkspace.getBlockById(ev.blockId); + if (block && block.type === 'trade' && ev.workspaceId === Blockly.mainWorkspace.id) { + const symbol = block.getFieldValue('SYMBOL_LIST'); + if (!symbol) return; + + this.pollForContracts(symbol).then(contracts => { + this.updateBarrierOffsetBlocks(contracts, false, false); // false false to maintain user's values on import + this.updatePredictionBlocks(contracts); + this.updateDurationLists(contracts, false, false); // false false to maintain user's values on import + }); + } + } else if (ev.type === Blockly.Events.MOVE) { + // Make sure tradeOptions-blocks are consistent with symbol/tradeType when re-added to root-block + if (!ev.oldParentId && ev.newParentId) { + const movedBlock = Blockly.mainWorkspace.getBlockById(ev.blockId); + const topParentBlock = findTopParentBlock(movedBlock); + + if (topParentBlock && topParentBlock.type === 'trade') { + const symbol = topParentBlock.getFieldValue('SYMBOL_LIST'); + if (!symbol) return; + + const getNestedTradeOptions = block => { + if (block.type === 'tradeOptions') { + this.pollForContracts(symbol).then(contracts => { + this.updateBarrierOffsetBlocks(contracts, false, false, [block.id]); + this.applyBarrierHandlebars('BARRIEROFFSETTYPE_LIST', [ev.blockId], true); + this.updatePredictionBlocks(contracts, [block.id]); + this.updateDurationLists(contracts, false, false, [block.id]); + }); + } + block.getChildren().forEach(childBlock => { + getNestedTradeOptions(childBlock); + }); + }; + getNestedTradeOptions(movedBlock); + } + } + } else if (ev.type === Blockly.Events.CHANGE) { // eslint-disable-next-line no-underscore-dangle - if (ev.oldValue !== ev.newValue && this.parentBlock_ !== null) { - const durationTypeList = this.getField('DURATIONTYPE_LIST'); - if (durationTypeList === null) return; + if (this.parentBlock_ !== null) { + const symbol = getParentValue(this, 'SYMBOL_LIST'); + if (!symbol) return; + + this.pollForContracts(symbol).then(contracts => { + if (ev.name === 'SYMBOL_LIST' && ev.oldValue !== ev.newValue) { + // Called to update duration options and set min durations + this.updateDurationLists(contracts, true, true); + } else if (['TRADETYPE_LIST'].includes(ev.name) && ev.oldValue !== ev.newValue) { + // Both are called to check if these blocks are required + this.updatePredictionBlocks(contracts); + this.updateBarrierOffsetBlocks(contracts, true, true); + // Handlebars for all tradeOptions barrier-blocks + this.applyBarrierHandlebars('BARRIEROFFSETTYPE_LIST', true); + // Called to default to smallest durations for symbol + this.updateDurationLists(contracts, false, true); + } else if (ev.name === 'DURATIONTYPE_LIST' && ev.oldValue !== ev.newValue) { + // Called to set barriers based on duration + this.updateBarrierOffsetBlocks(contracts, true, true, [ev.blockId]); + this.applyBarrierHandlebars('BARRIEROFFSETTYPE_LIST', true, [ev.blockId]); + // Called to set min durations for selected unit + this.updateDurationLists(contracts, false, true, [ev.blockId]); + } else if ( + ['BARRIEROFFSETTYPE_LIST', 'SECONDBARRIEROFFSETTYPE_LIST'].includes(ev.name) && + ev.oldValue !== ev.newValue + ) { + // Called to set default values based on barrier type + this.updateBarrierOffsetBlocks(contracts, false, true, [ev.blockId]); + // Suggests same type barriers + this.applyBarrierHandlebars(ev.name, false, [ev.blockId]); + } + updateInputList(this); + }); + } + } + }, + pollForContracts(symbol) { + return new Promise(resolve => { + const contractsForSymbol = haveContractsForSymbol(symbol); + if (!contractsForSymbol) { + // Register an event and use as a lock to avoid spamming API + const event = `contractsLoaded.${symbol}`; + if (!globalObserver.isRegistered(event)) { + globalObserver.register(event, () => {}); + getContractsAvailableForSymbol(symbol).then(contracts => { + globalObserver.unregisterAll(event); // Release the lock + resolve(contracts); + }); + } else { + // Request in progress, start polling localStorage until contracts are available. + const pollingFn = setInterval(() => { + const contracts = haveContractsForSymbol(symbol); + if (contracts) { + clearInterval(pollingFn); + resolve(contracts.available); + } + }, 100); + setTimeout(() => { + clearInterval(pollingFn); + resolve([]); + }, 10000); + } + } else { + resolve(contractsForSymbol.available); + } + }); + }, + updatePredictionBlocks(contracts, updateOnly = []) { + getBlocksByType('tradeOptions').forEach(tradeOptionsBlock => { + if (tradeOptionsBlock.disabled) return; + if (updateOnly.length && !updateOnly.includes(tradeOptionsBlock.id)) return; - const symbol = getSelectedSymbol(this); - const tradeType = getTradeType(this); + const predictionInput = tradeOptionsBlock.getInput('PREDICTION'); + if (!predictionInput) return; - const prevSelectedDuration = durationTypeList.getValue(); + const tradeType = getParentValue(tradeOptionsBlock, 'TRADETYPE_LIST'); + const predictionRange = getPredictionForContracts(contracts, tradeType); - Blockly.Events.recordUndo = false; - this.setFieldValue(translate('Loading...'), 'DURATIONTYPE_LIST'); - Blockly.Events.recordUndo = true; + hideInteractionsFromBlockly(() => { + if (!predictionRange.length) { + tradeOptionsBlock.removeInput('PREDICTION'); + return; + } + predictionInput.setVisible(true); - getAvailableDurations(symbol, tradeType).then(durations => { - Blockly.Events.recordUndo = false; - // Prevent UI flickering by only updating field if options have changed - // eslint-disable-next-line no-underscore-dangle - if (JSON.stringify(durationTypeList.menuGenerator_) !== JSON.stringify(durations)) { - durationTypeList.menuGenerator_ = durations; // eslint-disable-line no-underscore-dangle + // Attach shadow block with API-returned prediction-value (only if user hasn't defined a value) + if (!predictionInput.connection.isConnected()) { + predictionInput.attachShadowBlock(predictionRange[0], 'NUM', 'math_number'); + } + }); + }); + }, + updateBarrierOffsetBlocks(contracts, useDefaultType = false, setDefaultValue = false, updateOnly = []) { + getBlocksByType('tradeOptions').forEach(tradeOptionsBlock => { + if (tradeOptionsBlock.disabled) return; + if (updateOnly.length && !updateOnly.includes(tradeOptionsBlock.id)) return; + + const tradeType = getParentValue(tradeOptionsBlock, 'TRADETYPE_LIST'); + const selectedDuration = tradeOptionsBlock.getFieldValue('DURATIONTYPE_LIST'); + const selectedBarrierTypes = [ + tradeOptionsBlock.getFieldValue('BARRIEROFFSETTYPE_LIST'), + tradeOptionsBlock.getFieldValue('SECONDBARRIEROFFSETTYPE_LIST'), + ]; + const barriers = getBarriersForContracts(contracts, tradeType, selectedDuration, selectedBarrierTypes); + + hideInteractionsFromBlockly(() => { + const revealBarrierBlock = (barrierValue, inputName) => { + const barrierInput = tradeOptionsBlock.getInput(inputName); + if (barrierInput) { + barrierInput.setVisible(true); + if (!barrierInput.connection.isConnected()) { + barrierInput.attachShadowBlock(barrierValue, 'NUM', 'math_number'); + } else if (setDefaultValue) { + const connectedBlock = barrierInput.connection.targetBlock(); + if (connectedBlock.isShadow()) { + connectedBlock.setFieldValue(barrierValue, 'NUM'); + } + } } - // Maintain previously selected duration if possible (req for imported strategies) - const selectedValue = durationTypeList.menuGenerator_.find(d => d[1] === prevSelectedDuration); // eslint-disable-line no-underscore-dangle - if (selectedValue) { - this.setFieldValue(selectedValue[1], 'DURATIONTYPE_LIST'); + }; + + const barrierOffsetNames = ['BARRIEROFFSET', 'SECONDBARRIEROFFSET']; + const barrierLabels = [translate('High barrier'), translate('Low barrier')]; + const removeInput = inputName => tradeOptionsBlock.removeInput(inputName); + + const updateList = (list, options) => { + const prevSelectedType = options.find(option => option[1] === list.getValue()); + list.menuGenerator_ = options; // eslint-disable-line no-underscore-dangle, no-param-reassign + if (!useDefaultType && prevSelectedType) { + list.setValue(''); + list.setValue(prevSelectedType[1]); // eslint-disable-next-line no-underscore-dangle - } else if (durationTypeList.menuGenerator_.length) { - this.setFieldValue(durationTypeList.menuGenerator_[0][1], 'DURATIONTYPE_LIST'); // eslint-disable-line no-underscore-dangle + } else if (list.menuGenerator_.length) { + list.setValue(''); + list.setValue(list.menuGenerator_[0][1]); // eslint-disable-line no-underscore-dangle + } + }; + + if (!barriers.values.length) { + barrierOffsetNames.forEach(removeInput); + return; + } + + barriers.values.forEach((barrierValue, index) => { + const typeList = tradeOptionsBlock.getField(`${barrierOffsetNames[index]}TYPE_LIST`); + const typeInput = tradeOptionsBlock.getInput(barrierOffsetNames[index]); + const absoluteType = [[translate('Absolute'), 'absolute']]; + + if (selectedDuration === 'd') { + updateList(typeList, absoluteType); + } else if (barriers.allowBothTypes || barriers.allowAbsoluteType) { + updateList(typeList, [...config.barrierTypes, ...absoluteType]); } else { - this.setFieldValue(translate('Not available'), 'DURATIONTYPE_LIST'); + updateList(typeList, config.barrierTypes); } - Blockly.Events.recordUndo = true; + + if (barriers.values.length === 1) { + typeInput.fieldRow[0].setText(`${translate('Barrier')}:`); + } else { + typeInput.fieldRow[0].setText(`${barrierLabels[index]}:`); + } + revealBarrierBlock(barrierValue, barrierOffsetNames[index]); }); + barrierOffsetNames.slice(barriers.values.length).forEach(removeInput); + }); + }); + }, + updateDurationLists(contracts, useDefaultUnit = false, setMinDuration = false, updateOnly = []) { + getBlocksByType('tradeOptions').forEach(tradeOptionsBlock => { + if (tradeOptionsBlock.disabled) return; + if (updateOnly.length && !updateOnly.includes(tradeOptionsBlock.id)) return; + + const tradeType = getParentValue(tradeOptionsBlock, 'TRADETYPE_LIST'); + const durationTypeList = tradeOptionsBlock.getField('DURATIONTYPE_LIST'); + const selectedDuration = durationTypeList.getValue(); + + const durations = getDurationsForContracts(contracts, tradeType); + const durationOptions = durations.map(duration => [duration.label, duration.unit]); + + hideInteractionsFromBlockly(() => { + // Prevent UI flickering by only updating field only if options have changed + // eslint-disable-next-line no-underscore-dangle + if (JSON.stringify(durationTypeList.menuGenerator_) !== JSON.stringify(durationOptions)) { + durationTypeList.menuGenerator_ = durationOptions; // eslint-disable-line no-underscore-dangle + } + + // Set duration to previous selected duration (required for imported strategies) + // eslint-disable-next-line no-underscore-dangle + const prevSelectedDuration = durationTypeList.menuGenerator_.find(d => d[1] === selectedDuration); + if (!useDefaultUnit && prevSelectedDuration) { + durationTypeList.setValue(''); + durationTypeList.setValue(prevSelectedDuration[1]); + // eslint-disable-next-line no-underscore-dangle + } else if (durationTypeList.menuGenerator_.length) { + durationTypeList.setValue(''); + // eslint-disable-next-line no-underscore-dangle + durationTypeList.setValue(durationTypeList.menuGenerator_[0][1]); + } + + // Attach shadow block with min value (only when user hasn't already attached another output block) + if (durations.length) { + const durationInput = tradeOptionsBlock.getInput('DURATION'); + if (!durationInput.connection.isConnected()) { + durationInput.attachShadowBlock(durations[0].minimum, 'NUM', 'math_number'); + } else if (setMinDuration) { + const connectedBlock = durationInput.connection.targetBlock(); + const minDuration = durations.find(d => d.unit === selectedDuration); + + if (connectedBlock.isShadow() && minDuration) { + connectedBlock.setFieldValue(minDuration.minimum, 'NUM'); + } + } + } + }); + }); + }, + applyBarrierHandlebars(barrierFieldName, forceDistinct = false, updateOnly = []) { + getBlocksByType('tradeOptions').forEach(tradeOptionsBlock => { + if (tradeOptionsBlock.disabled) return; + if (updateOnly.length && !updateOnly.includes(tradeOptionsBlock.id)) return; + + const newValue = tradeOptionsBlock.getFieldValue(barrierFieldName); + const otherBarrierListName = () => { + if (barrierFieldName === 'BARRIEROFFSETTYPE_LIST') { + return 'SECONDBARRIEROFFSETTYPE_LIST'; + } + return 'BARRIEROFFSETTYPE_LIST'; + }; + + const otherBarrierList = tradeOptionsBlock.getField(otherBarrierListName()); + if (otherBarrierList) { + const otherBarrierType = otherBarrierList.getValue(); + if ( + config.barrierTypes.findIndex(t => t[1] === newValue) !== -1 && + (otherBarrierType === 'absolute' || forceDistinct) + ) { + const otherValue = config.barrierTypes.find(t => t[1] !== newValue); + otherBarrierList.setValue(otherValue[1]); + } else if (newValue === 'absolute' && otherBarrierType !== 'absolute') { + otherBarrierList.setValue('absolute'); + } } - } + }); }, }; Blockly.JavaScript.tradeOptions = block => { - const durationValue = expectValue(block, 'DURATION'); - const durationType = block.getFieldValue('DURATIONTYPE_LIST'); - const currency = block.getFieldValue('CURRENCY_LIST'); - const amount = expectValue(block, 'AMOUNT'); const tradeDefBlock = findTopParentBlock(block); if (!tradeDefBlock) { return ''; } - const oppositesName = tradeDefBlock.getFieldValue('TRADETYPE_LIST').toUpperCase(); + + const durationValue = expectValue(block, 'DURATION'); + const durationType = block.getFieldValue('DURATIONTYPE_LIST'); + const currency = block.getFieldValue('CURRENCY_LIST'); + const amount = expectValue(block, 'AMOUNT'); + + const isVisibleField = field => block.getInput(field) && block.getInput(field).isVisible(); + let predictionValue = 'undefined'; let barrierOffsetValue = 'undefined'; let secondBarrierOffsetValue = 'undefined'; - if (config.hasPrediction.indexOf(oppositesName) > -1) { + + if (isVisibleField('PREDICTION')) { predictionValue = expectValue(block, 'PREDICTION'); } - if ( - config.hasBarrierOffset.indexOf(oppositesName) > -1 || - config.hasSecondBarrierOffset.indexOf(oppositesName) > -1 - ) { + if (isVisibleField('BARRIEROFFSET')) { const barrierOffsetType = block.getFieldValue('BARRIEROFFSETTYPE_LIST'); const value = expectValue(block, 'BARRIEROFFSET'); - barrierOffsetValue = `${barrierOffsetType}${value}`; + barrierOffsetValue = barrierOffsetType === 'absolute' ? `'${value}'` : `'${barrierOffsetType}${value}'`; } - if (config.hasSecondBarrierOffset.indexOf(oppositesName) > -1) { + if (isVisibleField('SECONDBARRIEROFFSET')) { const barrierOffsetType = block.getFieldValue('SECONDBARRIEROFFSETTYPE_LIST'); const value = expectValue(block, 'SECONDBARRIEROFFSET'); - secondBarrierOffsetValue = `${barrierOffsetType}${value}`; + secondBarrierOffsetValue = + barrierOffsetType === 'absolute' ? `'${value}'` : `'${barrierOffsetType}${value}'`; } + const code = ` - Bot.start({ - limitations: BinaryBotPrivateLimitations, - duration: ${durationValue}, - duration_unit: '${durationType}', - currency: '${currency}', - amount: ${amount}, - prediction: ${predictionValue}, - barrierOffset: ${barrierOffsetValue}, - secondBarrierOffset: ${secondBarrierOffsetValue}, - }); - `; + Bot.start({ + limitations: BinaryBotPrivateLimitations, + duration: ${durationValue}, + duration_unit: '${durationType}', + currency: '${currency}', + amount: ${amount}, + prediction: ${predictionValue}, + barrierOffset: ${barrierOffsetValue}, + secondBarrierOffset: ${secondBarrierOffsetValue}, + }); + `; return code; }; }; diff --git a/src/botPage/view/blockly/customBlockly.js b/src/botPage/view/blockly/customBlockly.js index 11712f5ea2..4e9319a389 100644 --- a/src/botPage/view/blockly/customBlockly.js +++ b/src/botPage/view/blockly/customBlockly.js @@ -1,3 +1,5 @@ +import { translate } from '../../../common/utils/tools'; + /* eslint-disable */ Blockly.WorkspaceAudio.prototype.preload = function() {}; Blockly.FieldDropdown.prototype.render_ = function() { @@ -288,3 +290,47 @@ Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) { this.bindEvents_(htmlInput); }; +const originalContextMenuFn = Blockly.ContextMenu.show; +Blockly.ContextMenu.show = (e, menuOptions, rtl) => { + // Rename 'Clean up blocks' + menuOptions.some(option => { + if (option.text === Blockly.Msg.CLEAN_UP) { + option.text = translate('Rearrange vertically'); // eslint-disable-line no-param-reassign + return true; + } + return false; + }) && + /* Remove delete all blocks, but only when 'Clean up blocks' is available (i.e. workspace) + * This allows users to still delete root blocks containing blocks + */ + menuOptions.some((option, i) => { + if ( + option.text === Blockly.Msg.DELETE_BLOCK || + option.text.replace(/[0-9]+/, '%1') === Blockly.Msg.DELETE_X_BLOCKS + ) { + menuOptions.splice(i, 1); + return true; + } + return false; + }); + // Open the Elev.io widget when clicking 'Help' + // eslint-disable-next-line no-underscore-dangle + if (window._elev) { + menuOptions.some(option => { + if (option.text === Blockly.Msg.HELP) { + option.callback = () => window._elev.open(); // eslint-disable-line no-param-reassign, no-underscore-dangle + return true; + } + return false; + }); + } + originalContextMenuFn(e, menuOptions, rtl); +}; +Blockly.Input.prototype.attachShadowBlock = function(value, name, shadowBlockType) { + const shadowBlock = this.sourceBlock_.workspace.newBlock(shadowBlockType); + shadowBlock.setShadow(true); + shadowBlock.setFieldValue(value, name); // Refactor when using shadow block for strings in future + shadowBlock.outputConnection.connect(this.connection); + shadowBlock.initSvg(); + shadowBlock.render(); +}; diff --git a/src/botPage/view/blockly/index.js b/src/botPage/view/blockly/index.js index f446667107..c16b058cdb 100644 --- a/src/botPage/view/blockly/index.js +++ b/src/botPage/view/blockly/index.js @@ -415,43 +415,3 @@ while(true) { } /* eslint-enable */ } - -// Hooks to override default Blockly behaviour -/* eslint-disable no-unused-expressions */ -const originalContextMenuFn = Blockly.ContextMenu.show; -Blockly.ContextMenu.show = (e, menuOptions, rtl) => { - // Rename 'Clean up blocks' - menuOptions.some(option => { - if (option.text === Blockly.Msg.CLEAN_UP) { - option.text = translate('Rearrange vertically'); // eslint-disable-line no-param-reassign - return true; - } - return false; - }) && - /* Remove delete all blocks, but only when 'Clean up blocks' is available (i.e. workspace) - * This allows users to still delete root blocks containing blocks - */ - menuOptions.some((option, i) => { - if ( - option.text === Blockly.Msg.DELETE_BLOCK || - option.text.replace(/[0-9]+/, '%1') === Blockly.Msg.DELETE_X_BLOCKS - ) { - menuOptions.splice(i, 1); - return true; - } - return false; - }); - // Open the Elev.io widget when clicking 'Help' - // eslint-disable-next-line no-underscore-dangle - if (window._elev) { - menuOptions.some(option => { - if (option.text === Blockly.Msg.HELP) { - option.callback = () => window._elev.openHome(); // eslint-disable-line no-param-reassign, no-underscore-dangle - return true; - } - return false; - }); - } - originalContextMenuFn(e, menuOptions, rtl); -}; -/* eslint-enable */ diff --git a/src/botPage/view/blockly/utils.js b/src/botPage/view/blockly/utils.js index ece0b55396..237b7af0c9 100644 --- a/src/botPage/view/blockly/utils.js +++ b/src/botPage/view/blockly/utils.js @@ -489,3 +489,9 @@ export const loadRemote = blockObj => } } }); + +export const hideInteractionsFromBlockly = callback => { + Blockly.Events.recordUndo = false; + callback(); + Blockly.Events.recordUndo = true; +}; diff --git a/static/xml/main.xml b/static/xml/main.xml index 88140c3ad1..fb7fb2f874 100644 --- a/static/xml/main.xml +++ b/static/xml/main.xml @@ -4,11 +4,7 @@ t USD - - - 5 - - + 1