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
JavaScript
'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;
})