UNPKG

metaapi.cloud-sdk

Version:

SDK for MetaApi, a professional cloud forex API which includes MetaTrader REST API and MetaTrader websocket API. Supports both MetaTrader 5 (MT5) and MetaTrader 4 (MT4). CopyFactory copy trading API included. (https://metaapi.cloud)

1,002 lines 367 kB
'use strict'; function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _async_to_generator(fn) { return function() { var self = this, args = arguments; return new Promise(function(resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } function _define_property(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _object_spread(target) { for(var i = 1; i < arguments.length; i++){ var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === "function") { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function(key) { _define_property(target, key, source[key]); }); } return target; } import randomstring from 'randomstring'; import socketIO from 'socket.io-client'; import TimeoutError from '../timeoutError'; import { ValidationError, NotFoundError, InternalError, UnauthorizedError, TooManyRequestsError, ForbiddenError } from '../errorHandler'; import OptionsValidator from '../optionsValidator'; import NotSynchronizedError from './notSynchronizedError'; import NotConnectedError from './notConnectedError'; import TradeError from './tradeError'; import PacketOrderer from './packetOrderer'; import SynchronizationThrottler from './synchronizationThrottler'; import SubscriptionManager from './subscriptionManager'; import LoggerManager from '../../logger'; import any from 'promise.any'; import LatencyService from './latencyService'; import _ from 'lodash'; export * from './metaApiWebsocket.client.schemas'; let PacketLogger; if (typeof window === 'undefined') { PacketLogger = require('./packetLogger').default; } /** * MetaApi websocket API client (see https://metaapi.cloud/docs/client/websocket/overview/) */ let MetaApiWebsocketClient = class MetaApiWebsocketClient { /** * Restarts the account synchronization process on an out of order packet * @param {String} accountId account id * @param {Number} instanceIndex instance index * @param {Number} expectedSequenceNumber expected s/n * @param {Number} actualSequenceNumber actual s/n * @param {Object} packet packet data * @param {Date} receivedAt time the packet was received at */ onOutOfOrderPacket(accountId, instanceIndex, expectedSequenceNumber, actualSequenceNumber, packet, receivedAt) { const primaryAccountId = this._accountsByReplicaId[accountId]; if (this._subscriptionManager.isSubscriptionActive(accountId)) { const level = this._latencyService.getSynchronizedAccountInstances(primaryAccountId).length ? 'debug' : 'error'; this._logger[level]('MetaApi websocket client received an out of order ' + `packet type ${packet.type} for account id ${accountId}:${instanceIndex}. Expected s/n ` + `${expectedSequenceNumber} does not match the actual of ${actualSequenceNumber}`); this.ensureSubscribe(accountId, instanceIndex); } } /** * Patch server URL for use in unit tests * @param {String} url patched server URL */ set url(url) { this._url = url; } /** * Websocket client predefined region * @returns {String} predefined region */ get region() { return this._region; } /** * Returns the list of socket instance dictionaries * @return {Object[]} list of socket instance dictionaries */ get socketInstances() { return this._socketInstances; } /** * Returns the dictionary of socket instances by account ids * @return {Object} dictionary of socket instances by account ids */ get socketInstancesByAccounts() { return this._socketInstancesByAccounts; } /** * Returns the dictionary of account replicas by region * @return {Object} dictionary of account replicas by region */ get accountReplicas() { return this._accountReplicas; } /** * Returns the dictionary of primary account ids by replica ids * @return {Object} dictionary of primary account ids by replica ids */ get accountsByReplicaId() { return this._accountsByReplicaId; } /** * Returns clear account cache job. Used for tests * @return {Function} clear account cache job */ get clearAccountCacheJob() { return this._clearAccountCacheJob.bind(this); } /** * Returns latency service * @returns {LatencyService} latency service */ get latencyService() { return this._latencyService; } /** * Returns the list of subscribed account ids * @param {Number} instanceNumber instance index number * @param {String} socketInstanceIndex socket instance index * @param {String} region server region * @return {string[]} list of subscribed account ids */ subscribedAccountIds(instanceNumber, socketInstanceIndex, region) { const connectedIds = []; if (this._socketInstancesByAccounts[instanceNumber]) { Object.keys(this._connectedHosts).forEach((instanceId)=>{ const accountId = instanceId.split(':')[0]; const accountRegion = this.getAccountRegion(accountId); if (!connectedIds.includes(accountId) && this._socketInstancesByAccounts[instanceNumber][accountId] !== undefined && (this._socketInstancesByAccounts[instanceNumber][accountId] === socketInstanceIndex || socketInstanceIndex === undefined) && accountRegion === region) { connectedIds.push(accountId); } }); } return connectedIds; } /** * Returns websocket client connection status * @param {Number} instanceNumber instance index number * @param {Number} socketInstanceIndex socket instance index * @param {String} region server region * @returns {Boolean} websocket client connection status */ connected(instanceNumber, socketInstanceIndex, region) { const instance = this._socketInstances[region] && this._socketInstances[region][instanceNumber].length > socketInstanceIndex ? this._socketInstances[region][instanceNumber][socketInstanceIndex] : null; return instance && instance.socket && instance.socket.connected || false; } /** * Returns list of accounts assigned to instance * @param {Number} instanceNumber instance index number * @param {String} socketInstanceIndex socket instance index * @param {String} region server region * @returns */ _getAssignedAccounts(instanceNumber, socketInstanceIndex, region) { const accountIds = []; Object.keys(this._socketInstancesByAccounts[instanceNumber]).forEach((key)=>{ const accountRegion = this.getAccountRegion(key); if (accountRegion === region && this._socketInstancesByAccounts[instanceNumber][key] === socketInstanceIndex) { accountIds.push(key); } }); return accountIds; } /** * Returns account region by id * @param {String} accountId account id * @returns {String} account region */ getAccountRegion(accountId) { return this._regionsByAccounts[accountId] && this._regionsByAccounts[accountId].region; } /** * Adds account cache info * @param {String} accountId account id * @param {Object} replicas account replicas, including primary replica */ addAccountCache(accountId, replicas) { this._accountReplicas[accountId] = replicas; Object.keys(replicas).forEach((region)=>{ const replicaId = replicas[region]; if (!this._regionsByAccounts[replicaId]) { this._regionsByAccounts[replicaId] = { region, connections: 1, lastUsed: Date.now() }; } else { this._regionsByAccounts[replicaId].connections++; } this._accountsByReplicaId[replicaId] = accountId; }); this._logger.debug(`${accountId}: added account cache`); } /** * Updates account cache info * @param {String} accountId account id * @param {Object} replicas account replicas */ updateAccountCache(accountId, replicas) { const oldReplicas = this._accountReplicas[accountId]; if (oldReplicas) { const connectionCount = this._regionsByAccounts[accountId].connections; Object.keys(oldReplicas).forEach((region)=>{ const replicaId = replicas[region]; delete this._accountsByReplicaId[replicaId]; delete this._regionsByAccounts[replicaId]; }); this._accountReplicas[accountId] = replicas; Object.keys(replicas).forEach((region)=>{ const replicaId = replicas[region]; this._regionsByAccounts[replicaId] = { region, connections: connectionCount, lastUsed: Date.now() }; this._accountsByReplicaId[replicaId] = accountId; }); this._logger.debug(`${accountId}: updated account cache`); } } /** * Removes account region info * @param {String} accountId account id */ removeAccountCache(accountId) { var _this__regionsByAccounts_accountId; if (((_this__regionsByAccounts_accountId = this._regionsByAccounts[accountId]) === null || _this__regionsByAccounts_accountId === void 0 ? void 0 : _this__regionsByAccounts_accountId.connections) > 0) { this._regionsByAccounts[accountId].connections--; } } /** * Locks subscription for a socket instance based on TooManyRequestsError metadata * @param {Number} instanceNumber instance index number * @param {String} socketInstanceIndex socket instance index * @param {String} region server region * @param {Object} metadata TooManyRequestsError metadata */ lockSocketInstance(instanceNumber, socketInstanceIndex, region, metadata) { var _this = this; return _async_to_generator(function*() { if (metadata.type === 'LIMIT_ACCOUNT_SUBSCRIPTIONS_PER_USER') { _this._subscribeLock = { recommendedRetryTime: metadata.recommendedRetryTime, lockedAtAccounts: _this.subscribedAccountIds(instanceNumber, undefined, region).length, lockedAtTime: Date.now() }; } else { const subscribedAccounts = _this.subscribedAccountIds(instanceNumber, socketInstanceIndex, region); if (subscribedAccounts.length === 0) { const socketInstance = _this.socketInstances[region][instanceNumber][socketInstanceIndex]; socketInstance.socket.close(); yield _this._reconnect(instanceNumber, socketInstanceIndex, region); } else { const instance = _this.socketInstances[region][instanceNumber][socketInstanceIndex]; instance.subscribeLock = { recommendedRetryTime: metadata.recommendedRetryTime, type: metadata.type, lockedAtAccounts: subscribedAccounts.length }; } } })(); } /** * Connects to MetaApi server via socket.io protocol * @param {Number} instanceNumber instance index number * @param {String} region server region * @returns {Promise} promise which resolves when connection is established */ connect(instanceNumber, region) { var _this = this; return _async_to_generator(function*() { var _this__socketInstances, _region, _this__socketInstances_region, _instanceNumber; if (_this._region && region !== _this._region) { throw new ValidationError(`Trying to connect to ${region} region, but configured with ${_this._region}`); } let clientId = Math.random(); let resolve; let result = new Promise((res, rej)=>{ resolve = res; }); (_this__socketInstances = _this._socketInstances)[_region = region] || (_this__socketInstances[_region] = {}); (_this__socketInstances_region = _this._socketInstances[region])[_instanceNumber = instanceNumber] || (_this__socketInstances_region[_instanceNumber] = []); const socketInstanceIndex = _this._socketInstances[region][instanceNumber].length; const instance = { id: socketInstanceIndex, reconnectWaitTime: _this._socketMinimumReconnectTimeout, connected: false, requestResolves: {}, resolved: false, connectResult: result, sessionId: randomstring.generate(32), isReconnecting: false, socket: null, synchronizationThrottler: new SynchronizationThrottler(_this, socketInstanceIndex, instanceNumber, region, _this._synchronizationThrottlerOpts), subscribeLock: null, instanceNumber }; instance.connected = true; _this._socketInstances[region][instanceNumber].push(instance); instance.synchronizationThrottler.start(); const serverUrl = yield _this._getServerUrl(instanceNumber, socketInstanceIndex, region); const socketInstance = socketIO(serverUrl, { path: '/ws', reconnection: true, reconnectionDelay: 1000, reconnectionDelayMax: 5000, reconnectionAttempts: Infinity, timeout: _this._connectTimeout, extraHeaders: { 'Client-Id': clientId }, query: { 'auth-token': _this._token, clientId: clientId, protocol: 3 } }); instance.socket = socketInstance; socketInstance.on('connect', /*#__PURE__*/ _async_to_generator(function*() { // eslint-disable-next-line no-console _this._logger.info(`${region}:${instanceNumber}: MetaApi websocket client connected to the MetaApi server`); instance.reconnectWaitTime = _this._socketMinimumReconnectTimeout; instance.isReconnecting = false; if (!instance.resolved) { instance.resolved = true; resolve(); } else { yield _this._fireReconnected(instanceNumber, instance.id, region); } if (!instance.connected) { instance.socket.close(); } })); socketInstance.on('reconnect', /*#__PURE__*/ _async_to_generator(function*() { instance.isReconnecting = false; _this._logger.info(`${region}:${instanceNumber}: MetaApi websocket client reconnected`); yield _this._fireReconnected(instanceNumber, instance.id, region); })); socketInstance.on('connect_error', function() { var _ref = _async_to_generator(function*(err) { // eslint-disable-next-line no-console _this._logger.error(`${region}:${instanceNumber}: MetaApi websocket client connection error`, err); instance.isReconnecting = false; yield _this._reconnect(instanceNumber, instance.id, region); }); return function(err) { return _ref.apply(this, arguments); }; }()); socketInstance.on('connect_timeout', function() { var _ref = _async_to_generator(function*(timeout) { // eslint-disable-next-line no-console _this._logger.error(`${region}:${instanceNumber}: MetaApi websocket client connection timeout`); instance.isReconnecting = false; if (!instance.resolved) { yield _this._reconnect(instanceNumber, instance.id, region); } }); return function(timeout) { return _ref.apply(this, arguments); }; }()); socketInstance.on('disconnect', function() { var _ref = _async_to_generator(function*(reason) { instance.synchronizationThrottler.onDisconnect(); // eslint-disable-next-line no-console _this._logger.info(`${region}:${instanceNumber}: MetaApi websocket client disconnected from the ` + `MetaApi server because of ${reason}`); instance.isReconnecting = false; yield _this._reconnect(instanceNumber, instance.id, region); }); return function(reason) { return _ref.apply(this, arguments); }; }()); socketInstance.on('error', function() { var _ref = _async_to_generator(function*(error) { // eslint-disable-next-line no-console _this._logger.error(`${region}:${instanceNumber}: MetaApi websocket client error`, error); instance.isReconnecting = false; yield _this._reconnect(instanceNumber, instance.id, region); }); return function(error) { return _ref.apply(this, arguments); }; }()); socketInstance.on('response', (data)=>{ if (typeof data === 'string') { data = JSON.parse(data); } _this._logger.debug(()=>`${data.accountId}: Response received: ${JSON.stringify({ requestId: data.requestId, timestamps: data.timestamps })}`); let requestResolve = instance.requestResolves[data.requestId] || { resolve: ()=>{}, reject: ()=>{} }; delete instance.requestResolves[data.requestId]; _this._convertIsoTimeToDate(data); requestResolve.resolve(data); if (data.timestamps && requestResolve.type) { data.timestamps.clientProcessingFinished = new Date(); for (let listener of _this._latencyListeners){ Promise.resolve().then(()=>requestResolve.type === 'trade' ? listener.onTrade(data.accountId, data.timestamps) : listener.onResponse(data.accountId, requestResolve.type, data.timestamps)).catch((error)=>_this._logger.error('Failed to process onResponse event for account ' + data.accountId + ', request type ' + requestResolve.type, error)); } } }); socketInstance.on('processingError', (data)=>{ let requestResolve = instance.requestResolves[data.requestId] || { resolve: ()=>{}, reject: ()=>{} }; delete instance.requestResolves[data.requestId]; requestResolve.reject(_this._convertError(data)); }); // eslint-disable-next-line complexity socketInstance.on('synchronization', function() { var _ref = _async_to_generator(function*(data) { var _this__regionsByAccounts, _data_accountId; if (typeof data === 'string') { data = JSON.parse(data); } if (data.instanceIndex && data.instanceIndex !== instanceNumber) { _this._logger.trace(()=>`${data.accountId}:${data.instanceNumber}: received packet with wrong instance ` + `index via a socket with instance number of ${instanceNumber}, data=${JSON.stringify({ type: data.type, sequenceNumber: data.sequenceNumber, sequenceTimestamp: data.sequenceTimestamp, synchronizationId: data.synchronizationId, application: data.application, host: data.host, specificationsUpdated: data.specificationsUpdated, positionsUpdated: data.positionsUpdated, ordersUpdated: data.ordersUpdated, specifications: data.specifications ? (data.specifications || []).length : undefined })}`); return; } (_this__regionsByAccounts = _this._regionsByAccounts)[_data_accountId = data.accountId] || (_this__regionsByAccounts[_data_accountId] = { region, connections: 0, lastUsed: Date.now() }); _this._logger.trace(()=>`${data.accountId}:${data.instanceIndex}: Sync packet received: ${JSON.stringify({ type: data.type, sequenceNumber: data.sequenceNumber, sequenceTimestamp: data.sequenceTimestamp, synchronizationId: data.synchronizationId, application: data.application, host: data.host, specificationsUpdated: data.specificationsUpdated, positionsUpdated: data.positionsUpdated, ordersUpdated: data.ordersUpdated, specifications: data.specifications ? (data.specifications || []).length : undefined })}, ` + `active listeners: ${(_this._synchronizationListeners[data.accountId] || []).length}`); let activeSynchronizationIds = instance.synchronizationThrottler.activeSynchronizationIds; if (!data.synchronizationId || activeSynchronizationIds.includes(data.synchronizationId)) { if (_this._packetLogger) { yield _this._packetLogger.logPacket(data); } const ignoredPacketTypes = [ 'disconnected', 'status', 'keepalive' ]; if (!_this._subscriptionManager.isSubscriptionActive(data.accountId) && !ignoredPacketTypes.includes(data.type)) { _this._logger.debug(`${data.accountId}: Packet arrived to inactive connection, attempting` + ` unsubscribe, packet: ${data.type}`); if (_this._throttleRequest('unsubscribe', data.accountId, data.instanceIndex, _this._unsubscribeThrottlingInterval)) { _this.unsubscribe(data.accountId).catch((err)=>{ _this._logger.warn(`${data.accountId}:${data.instanceIndex || 0}: failed to unsubscribe`, err); }); } return; } _this._convertIsoTimeToDate(data); } else { data.type = 'noop'; } _this.queuePacket(instance, data); }); return function(data) { return _ref.apply(this, arguments); }; }()); socketInstance.on('metadata', (data)=>{ instance.clientApiHostname = data.clientApiHostname; }); return result; })(); } /** * Closes connection to MetaApi server */ close() { Object.keys(this._socketInstances).forEach((region)=>{ Object.keys(this._socketInstances[region]).forEach((instanceNumber)=>{ this._socketInstances[region][instanceNumber].forEach(function() { var _ref = _async_to_generator(function*(instance) { if (instance.connected) { instance.connected = false; yield instance.socket.close(); for (let requestResolve of Object.values(instance.requestResolves)){ requestResolve.reject(new Error('MetaApi connection closed')); } instance.requestResolves = {}; } }); return function(instance) { return _ref.apply(this, arguments); }; }()); this._socketInstancesByAccounts[instanceNumber] = {}; this._socketInstances[region][instanceNumber] = []; }); }); this._synchronizationListeners = {}; this._latencyListeners = []; this._packetOrderer.stop(); } /** * Stops the client */ stop() { clearInterval(this._clearAccountCacheInterval); clearInterval(this._clearInactiveSyncDataInterval); this._latencyService.stop(); } /** * Returns account information for a specified MetaTrader account. * @param {String} accountId id of the MetaTrader account to return information for * @param {GetAccountInformationOptions} [options] additional request options * @returns {Promise<MetatraderAccountInformation>} promise resolving with account information */ getAccountInformation(accountId, options) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, _object_spread({ application: 'RPC', type: 'getAccountInformation' }, options)); return response.accountInformation; })(); } /** * Returns positions for a specified MetaTrader account. * @param {String} accountId id of the MetaTrader account to return information for * @param {GetPositionsOptions} [options] additional request options * @returns {Promise<Array<MetatraderPosition>} promise resolving with array of open positions */ getPositions(accountId, options) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, _object_spread({ application: 'RPC', type: 'getPositions' }, options)); return response.positions; })(); } /** * Returns specific position for a MetaTrader account. * @param {String} accountId id of the MetaTrader account to return information for * @param {String} positionId position id * @param {GetPositionOptions} [options] additional request options * @return {Promise<MetatraderPosition>} promise resolving with MetaTrader position found */ getPosition(accountId, positionId, options) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, _object_spread({ application: 'RPC', type: 'getPosition', positionId }, options)); return response.position; })(); } /** * Returns open orders for a specified MetaTrader account. * @param {String} accountId id of the MetaTrader account to return information for * @param {GetOrdersOptions} [options] additional request options * @return {Promise<Array<MetatraderOrder>>} promise resolving with open MetaTrader orders */ getOrders(accountId, options) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, _object_spread({ application: 'RPC', type: 'getOrders' }, options)); return response.orders; })(); } /** * Returns specific open order for a MetaTrader account. * @param {String} accountId id of the MetaTrader account to return information for * @param {String} orderId order id (ticket number) * @param {GetOrderOptions} [options] additional request options * @return {Promise<MetatraderOrder>} promise resolving with metatrader order found */ getOrder(accountId, orderId, options) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, _object_spread({ application: 'RPC', type: 'getOrder', orderId }, options)); return response.order; })(); } /** * MetaTrader history orders search query response * @typedef {Object} MetatraderHistoryOrders * @property {Array<MetatraderOrder>} historyOrders array of history orders returned * @property {Boolean} synchronizing flag indicating that history order initial synchronization is still in progress * and thus search results may be incomplete */ /** * Returns the history of completed orders for a specific ticket number. * @param {String} accountId id of the MetaTrader account to return information for * @param {String} ticket ticket number (order id) * @returns {Promise<MetatraderHistoryOrders>} promise resolving with request results containing history orders found */ getHistoryOrdersByTicket(accountId, ticket) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, { application: 'RPC', type: 'getHistoryOrdersByTicket', ticket }); return { historyOrders: response.historyOrders, synchronizing: response.synchronizing }; })(); } /** * Returns the history of completed orders for a specific position id * @param {String} accountId id of the MetaTrader account to return information for * @param {String} positionId position id * @returns {Promise<MetatraderHistoryOrders>} promise resolving with request results containing history orders found */ getHistoryOrdersByPosition(accountId, positionId) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, { application: 'RPC', type: 'getHistoryOrdersByPosition', positionId }); return { historyOrders: response.historyOrders, synchronizing: response.synchronizing }; })(); } /** * Returns the history of completed orders for a specific time range * @param {String} accountId id of the MetaTrader account to return information for * @param {Date} startTime start of time range, inclusive * @param {Date} endTime end of time range, exclusive * @param {Number} offset pagination offset, default is 0 * @param {Number} limit pagination limit, default is 1000 * @returns {Promise<MetatraderHistoryOrders>} promise resolving with request results containing history orders found */ getHistoryOrdersByTimeRange(accountId, startTime, endTime, offset = 0, limit = 1000) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, { application: 'RPC', type: 'getHistoryOrdersByTimeRange', startTime, endTime, offset, limit }); return { historyOrders: response.historyOrders, synchronizing: response.synchronizing }; })(); } /** * MetaTrader history deals search query response * @typedef {Object} MetatraderDeals * @property {Array<MetatraderDeal>} deals array of history deals returned * @property {Boolean} synchronizing flag indicating that deal initial synchronization is still in progress * and thus search results may be incomplete */ /** * MetaTrader deal * @typedef {Object} MetatraderDeal * @property {String} id deal id (ticket number) * @property {String} type deal type (one of DEAL_TYPE_BUY, DEAL_TYPE_SELL, DEAL_TYPE_BALANCE, DEAL_TYPE_CREDIT, * DEAL_TYPE_CHARGE, DEAL_TYPE_CORRECTION, DEAL_TYPE_BONUS, DEAL_TYPE_COMMISSION, DEAL_TYPE_COMMISSION_DAILY, * DEAL_TYPE_COMMISSION_MONTHLY, DEAL_TYPE_COMMISSION_AGENT_DAILY, DEAL_TYPE_COMMISSION_AGENT_MONTHLY, * DEAL_TYPE_INTEREST, DEAL_TYPE_BUY_CANCELED, DEAL_TYPE_SELL_CANCELED, DEAL_DIVIDEND, DEAL_DIVIDEND_FRANKED, * DEAL_TAX). See https://www.mql5.com/en/docs/constants/tradingconstants/dealproperties#enum_deal_type * @property {String} entryType deal entry type (one of DEAL_ENTRY_IN, DEAL_ENTRY_OUT, DEAL_ENTRY_INOUT, * DEAL_ENTRY_OUT_BY). See https://www.mql5.com/en/docs/constants/tradingconstants/dealproperties#enum_deal_entry * @property {String} [symbol] symbol deal relates to * @property {Number} [magic] deal magic number, identifies the EA which initiated the deal * @property {Date} time time the deal was conducted at * @property {String} brokerTime time time the deal was conducted at, in broker timezone, YYYY-MM-DD HH:mm:ss.SSS format * @property {Number} [volume] deal volume * @property {Number} [price] the price the deal was conducted at * @property {Number} [commission] deal commission * @property {Number} [swap] deal swap * @property {Number} profit deal profit * @property {String} [positionId] id of position the deal relates to * @property {String} [orderId] id of order the deal relates to * @property {String} [comment] deal comment. The sum of the line lengths of the comment and the clientId * must be less than or equal to 26. For more information see https://metaapi.cloud/docs/client/clientIdUsage/ * @property {String} [brokerComment] current comment value on broker side (possibly overriden by the broker) * @property {String} [clientId] client-assigned id. The id value can be assigned when submitting a trade and * will be present on position, history orders and history deals related to the trade. You can use this field to bind * your trades to objects in your application and then track trade progress. The sum of the line lengths of the * comment and the clientId must be less than or equal to 26. For more information see * https://metaapi.cloud/docs/client/clientIdUsage/ * @property {String} platform platform id (mt4 or mt5) * @property {String} [reason] optional deal execution reason. One of DEAL_REASON_CLIENT, DEAL_REASON_MOBILE, * DEAL_REASON_WEB, DEAL_REASON_EXPERT, DEAL_REASON_SL, DEAL_REASON_TP, DEAL_REASON_SO, DEAL_REASON_ROLLOVER, * DEAL_REASON_VMARGIN, DEAL_REASON_SPLIT, DEAL_REASON_UNKNOWN. See * https://www.mql5.com/en/docs/constants/tradingconstants/dealproperties#enum_deal_reason. * @property {Number} [accountCurrencyExchangeRate] current exchange rate of account currency into account base * currency (USD if you did not override it) * @property {number} [stopLoss] deal stop loss. For MT5 opening deal this is the SL of the order opening the * position. For MT4 deals or MT5 closing deal this is the last known position SL. * @property {number} [takeProfit] deal take profit. For MT5 opening deal this is the TP of the order opening the * position. For MT4 deals or MT5 closing deal this is the last known position TP. */ /** * Returns history deals with a specific ticket number * @param {String} accountId id of the MetaTrader account to return information for * @param {String} ticket ticket number (deal id for MT5 or order id for MT4) * @returns {Promise<MetatraderDeals>} promise resolving with request results containing deals found */ getDealsByTicket(accountId, ticket) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, { application: 'RPC', type: 'getDealsByTicket', ticket }); return { deals: response.deals, synchronizing: response.synchronizing }; })(); } /** * Returns history deals for a specific position id * @param {String} accountId id of the MetaTrader account to return information for * @param {String} positionId position id * @returns {Promise<MetatraderDeals>} promise resolving with request results containing deals found */ getDealsByPosition(accountId, positionId) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, { application: 'RPC', type: 'getDealsByPosition', positionId }); return { deals: response.deals, synchronizing: response.synchronizing }; })(); } /** * Returns history deals with for a specific time range * @param {String} accountId id of the MetaTrader account to return information for * @param {Date} startTime start of time range, inclusive * @param {Date} endTime end of time range, exclusive * @param {Number} offset pagination offset, default is 0 * @param {Number} limit pagination limit, default is 1000 * @returns {Promise<MetatraderDeals>} promise resolving with request results containing deals found */ getDealsByTimeRange(accountId, startTime, endTime, offset = 0, limit = 1000) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, { application: 'RPC', type: 'getDealsByTimeRange', startTime, endTime, offset, limit }); return { deals: response.deals, synchronizing: response.synchronizing }; })(); } /** * Clears the order and transaction history of a specified application and removes the application * @param {String} accountId id of the MetaTrader account to remove history and application for * @return {Promise} promise resolving when the history is cleared */ removeApplication(accountId) { return this.rpcRequest(accountId, { type: 'removeApplication' }); } /** * MetaTrader trade response * @typedef {Object} MetatraderTradeResponse * @property {Number} numericCode numeric response code, see * https://www.mql5.com/en/docs/constants/errorswarnings/enum_trade_return_codes and * https://book.mql4.com/appendix/errors. Response codes which indicate success are 0, 10008-10010, 10025. The rest * codes are errors * @property {String} stringCode string response code, see * https://www.mql5.com/en/docs/constants/errorswarnings/enum_trade_return_codes and * https://book.mql4.com/appendix/errors. Response codes which indicate success are ERR_NO_ERROR, * TRADE_RETCODE_PLACED, TRADE_RETCODE_DONE, TRADE_RETCODE_DONE_PARTIAL, TRADE_RETCODE_NO_CHANGES. The rest codes are * errors. * @property {String} message human-readable response message * @property {String} orderId order id which was created/modified during the trade * @property {String} positionId position id which was modified during the trade */ /** * Execute a trade on a connected MetaTrader account * @param {String} accountId id of the MetaTrader account to execute trade for * @param {MetatraderTrade} trade trade to execute (see docs for possible trade types) * @param {String} [application] application to use * @param {String} [reliability] account reliability * @returns {Promise<MetatraderTradeResponse>} promise resolving with trade result * @throws {TradeError} on trade error, check error properties for error code details */ // eslint-disable-next-line complexity trade(accountId, trade, application, reliability) { var _this = this; return _async_to_generator(function*() { let response; if (application === 'RPC') { response = yield _this.rpcRequest(accountId, { type: 'trade', trade, application }); } else { response = yield _this.rpcRequestAllInstances(accountId, { type: 'trade', trade, application: application || _this._application, requestId: randomstring.generate(32) }, reliability); } response.response = response.response || {}; response.response.stringCode = response.response.stringCode || response.response.description; response.response.numericCode = response.response.numericCode !== undefined ? response.response.numericCode : response.response.error; if ([ 'ERR_NO_ERROR', 'TRADE_RETCODE_PLACED', 'TRADE_RETCODE_DONE', 'TRADE_RETCODE_DONE_PARTIAL', 'TRADE_RETCODE_NO_CHANGES' ].includes(response.response.stringCode || response.response.description)) { return response.response; } else { throw new TradeError(response.response.message, response.response.numericCode, response.response.stringCode); } })(); } /** * Creates a task that ensures the account gets subscribed to the server * @param {String} accountId account id to subscribe * @param {Number} instanceNumber instance index number */ ensureSubscribe(accountId, instanceNumber) { this._subscriptionManager.scheduleSubscribe(accountId, instanceNumber); } /** * Subscribes to the Metatrader terminal events * @param {String} accountId id of the MetaTrader account to subscribe to * @param {Number} instanceNumber instance index number * @returns {Promise} promise which resolves when subscription started */ subscribe(accountId, instanceNumber) { return this._subscriptionManager.subscribe(accountId, instanceNumber); } /** * Requests the terminal to start synchronization process * @param {String} accountId id of the MetaTrader account to synchronize * @param {Number} instanceIndex instance index * @param {String} host name of host to synchronize with * @param {String} synchronizationId synchronization request id * @param {Date} startingHistoryOrderTime from what date to start synchronizing history orders from. If not specified, * the entire order history will be downloaded. * @param {Date} startingDealTime from what date to start deal synchronization from. If not specified, then all * history deals will be downloaded. * @param {Function} getHashes function to get terminal state hashes * @returns {Promise} promise which resolves when synchronization started */ synchronize(accountId, instanceIndex, host, synchronizationId, startingHistoryOrderTime, startingDealTime, hashes) { var _this = this; return _async_to_generator(function*() { if (_this._getSocketInstanceByAccount(accountId, instanceIndex) === undefined) { _this._logger.debug(`${accountId}:${instanceIndex}: creating socket instance on synchronize`); yield _this._createSocketInstanceByAccount(accountId, instanceIndex); } const syncThrottler = _this._getSocketInstanceByAccount(accountId, instanceIndex).synchronizationThrottler; _this._synchronizationHashes[synchronizationId] = hashes; _this._synchronizationHashes[synchronizationId].lastUpdated = Date.now(); return syncThrottler.scheduleSynchronize(accountId, { requestId: synchronizationId, version: 2, type: 'synchronize', startingHistoryOrderTime, startingDealTime, instanceIndex, host }, hashes); })(); } /** * Waits for server-side terminal state synchronization to complete * @param {String} accountId id of the MetaTrader account to synchronize * @param {Number} [instanceNumber] instance index number * @param {String} applicationPattern MetaApi application regular expression pattern, default is .* * @param {Number} timeoutInSeconds timeout in seconds, default is 300 seconds * @param {String} [application] application to synchronize with * @returns {Promise} promise which resolves when synchronization started */ waitSynchronized(accountId, instanceNumber, applicationPattern, timeoutInSeconds, application) { return this.rpcRequest(accountId, { type: 'waitSynchronized', applicationPattern, timeoutInSeconds, instanceIndex: instanceNumber, application: application || this._application }, timeoutInSeconds + 1); } /** * Market data subscription * @typedef {Object} MarketDataSubscription * @property {string} type subscription type, one of quotes, candles, ticks, or marketDepth * @property {string} [timeframe] when subscription type is candles, defines the timeframe according to which the * candles must be generated. Allowed values for MT5 are 1m, 2m, 3m, 4m, 5m, 6m, 10m, 12m, 15m, 20m, 30m, 1h, 2h, 3h, * 4h, 6h, 8h, 12h, 1d, 1w, 1mn. Allowed values for MT4 are 1m, 5m, 15m 30m, 1h, 4h, 1d, 1w, 1mn * @property {number} [intervalInMilliseconds] defines how frequently the terminal will stream data to client. If not * set, then the value configured in account will be used */ /** * Subscribes on market data of specified symbol * @param {String} accountId id of the MetaTrader account * @param {String} symbol symbol (e.g. currency pair or an index) * @param {Array<MarketDataSubscription>} subscriptions array of market data subscription to create or update * @param {String} [reliability] account reliability * @returns {Promise} promise which resolves when subscription request was processed */ subscribeToMarketData(accountId, symbol, subscriptions, reliability) { return this.rpcRequestAllInstances(accountId, { type: 'subscribeToMarketData', symbol, subscriptions }, reliability); } /** * Refreshes market data subscriptions on the server to prevent them from expiring * @param {String} accountId id of the MetaTrader account * @param {Number} instanceNumber instance index number * @param {Array} subscriptions array of subscriptions to refresh */ refreshMarketDataSubscriptions(accountId, instanceNumber, subscriptions) { return this.rpcRequest(accountId, { type: 'refreshMarketDataSubscriptions', subscriptions, instanceIndex: instanceNumber }); } /** * Market data unsubscription * @typedef {Object} MarketDataUnsubscription * @property {string} type subscription type, one of quotes, candles, ticks, or marketDepth */ /** * Unsubscribes from market data of specified symbol * @param {String} accountId id of the MetaTrader account * @param {String} symbol symbol (e.g. currency pair or an index) * @param {Array<MarketDataUnsubscription>} subscriptions array of subscriptions to cancel * @param {String} [reliability] account reliability * @returns {Promise} promise which resolves when unsubscription request was processed */ unsubscribeFromMarketData(accountId, symbol, subscriptions, reliability) { return this.rpcRequestAllInstances(accountId, { type: 'unsubscribeFromMarketData', symbol, subscriptions }, reliability); } /** * Retrieves symbols available on an account * @param {String} accountId id of the MetaTrader account to retrieve symbols for * @returns {Promise<Array<string>>} promise which resolves when symbols are retrieved */ getSymbols(accountId) { var _this = this; return _async_to_generator(function*() { let response = yield _this.rpcRequest(accountId, { application: 'RPC', type: 'getSymbols' }); return response.symbols; })