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,102 lines (1,029 loc) • 109 kB
text/typescript
'use strict';
import randomstring from 'randomstring';
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, {SynchronizationThrottlerOpts} from './synchronizationThrottler';
import SubscriptionManager from './subscriptionManager';
import LoggerManager from '../../logger';
import any from 'promise.any';
import LatencyService from './latencyService';
import MetatraderAccount from '../../metaApi/metatraderAccount';
import DomainClient from '../domain.client';
import _ from 'lodash';
import ClientStickySocket from '../../packages/sticky-sockets/clientStickySocket';
import {RetryOpts} from '../../metaApi/metaApi';
import {PacketLoggerOpts} from './packetLogger';
import StickySocketConnection from '../../packages/sticky-sockets/stickySocketConnection';
import {Logger} from '../../logger';
import * as helpers from '../../helpers/helpers';
export * from './metaApiWebsocket.client.schemas';
let PacketLogger;
if (typeof window === 'undefined') { // don't import PacketLogger for browser version
PacketLogger = require('./packetLogger').default;
}
/**
* MetaApi websocket API client (see https://metaapi.cloud/docs/client/websocket/overview/)
*/
class MetaApiWebsocketClient {
private _domainClient: DomainClient;
private _application: any;
private _region: any;
private _hostname: string;
private _metaApi: any;
private _url: null;
private _requestTimeout: number;
private _connectTimeout: number;
private _retries: any;
private _minRetryDelayInSeconds: number;
private _maxRetryDelayInSeconds: number;
private _maxAccountsPerInstance: number;
private _subscribeCooldownInSeconds: any;
private _sequentialEventProcessing: boolean;
private _useSharedClientApi: any;
private _unsubscribeThrottlingInterval: number;
private _socketMinimumReconnectTimeout: number;
private _latencyService: LatencyService;
private _token: any;
private _synchronizationListeners: {};
private _latencyListeners: any[];
private _reconnectListeners: any[];
private _connectedHosts: {};
private _socketInstances: SocketInstances = {};
private _socketInstancesByAccounts: {[instanceNumber: number]: {[accountId: string]: number}} = {};
private _regionsByAccounts: RegionsByAccounts = {};
private _accountsByReplicaId: MetaApiWebsocketClient.AccountsByReplica = {};
private _accountReplicas: MetaApiWebsocketClient.AccountReplicas = {};
private _synchronizationThrottlerOpts: any;
private _subscriptionManager: any;
private _statusTimers: {};
private _eventQueues: {[accountId: string]: Array<() => Promise<any>>} = {};
private _synchronizationFlags: {[synchronizationId: string]: SynchronizationFlag} = {};
private _synchronizationIdByInstance: {};
private _subscribeLock: any;
private _firstConnect: boolean;
private _lastRequestsTime: {};
private _packetOrderer: PacketOrderer;
private _synchronizationHashes: {};
private _updateEvents: {[instanceId: string]: Packet[]} = {};
private _packetLogger: any;
private _logger: Logger;
private _clearAccountCacheInterval: NodeJS.Timeout;
private _clearInactiveSyncDataInterval: NodeJS.Timeout;
private _useNativeSocketIoServer: boolean;
/**
* Constructs MetaApi websocket API client instance
* @param {MetaApi} metaApi metaApi instance
* @param {DomainClient} domainClient domain client
* @param {String} token authorization token
* @param {MetaApiWebsocketClientOptions} opts websocket client options
*/
// eslint-disable-next-line complexity,max-statements
constructor(metaApi, domainClient, token, opts: MetaApiWebsocketClient.Options = {}) {
const validator = new OptionsValidator();
opts = opts || {};
opts.packetOrderingTimeout = validator.validateNonZero(opts.packetOrderingTimeout, 60, 'packetOrderingTimeout');
opts.synchronizationThrottler = opts.synchronizationThrottler || {};
this._domainClient = domainClient;
this._application = opts.application || 'MetaApi';
this._region = opts.region;
this._hostname = 'mt-client-api-v1';
this._metaApi = metaApi;
this._url = null;
this._requestTimeout = validator.validateNonZero(opts.requestTimeout, 60, 'requestTimeout') * 1000;
this._connectTimeout = validator.validateNonZero(opts.connectTimeout, 60, 'connectTimeout') * 1000;
const retryOpts = opts.retryOpts || {};
this._retries = validator.validateNumber(retryOpts.retries, 5, 'retryOpts.retries');
this._minRetryDelayInSeconds = validator.validateNonZero(retryOpts.minDelayInSeconds, 1,
'retryOpts.minDelayInSeconds');
this._maxRetryDelayInSeconds = validator.validateNonZero(retryOpts.maxDelayInSeconds, 30,
'retryOpts.maxDelayInSeconds');
this._maxAccountsPerInstance = 100;
this._subscribeCooldownInSeconds = validator.validateNonZero(retryOpts.subscribeCooldownInSeconds, 600,
'retryOpts.subscribeCooldownInSeconds');
this._sequentialEventProcessing = true;
this._useSharedClientApi = validator.validateBoolean(opts.useSharedClientApi, false, 'useSharedClientApi');
this._unsubscribeThrottlingInterval = validator.validateNonZero(opts.unsubscribeThrottlingIntervalInSeconds, 10,
'unsubscribeThrottlingIntervalInSeconds') * 1000;
this._useNativeSocketIoServer = validator.validateBoolean(opts.useNativeSocketIoServer, false,
'useNativeSocketIoServer');
this._socketMinimumReconnectTimeout = validator.validateNumber(opts.minReconnectTimeoutInMs, 500,
'minReconnectTimeoutInMs');
this._latencyService = new LatencyService(this, token, this._connectTimeout);
this._token = token;
this._synchronizationListeners = {};
this._latencyListeners = [];
this._reconnectListeners = [];
this._connectedHosts = {};
this._synchronizationThrottlerOpts = opts.synchronizationThrottler;
this._subscriptionManager = new SubscriptionManager(this, metaApi);
this._statusTimers = {};
this._synchronizationIdByInstance = {};
this._subscribeLock = null;
this._firstConnect = true;
this._lastRequestsTime = {};
this._packetOrderer = new PacketOrderer(this, opts.packetOrderingTimeout);
this._packetOrderer.start();
this._synchronizationHashes = {};
if (opts.packetLogger?.enabled) {
this._packetLogger = new PacketLogger(opts.packetLogger);
this._packetLogger.start();
}
this._logger = LoggerManager.getLogger('MetaApiWebsocketClient');
if (!opts.disableInternalJobs) {
this._clearAccountCacheInterval = setInterval(this._clearAccountCacheJob.bind(this), 30 * 60 * 1000);
this._clearInactiveSyncDataInterval = setInterval(this._clearInactiveSyncDataJob.bind(this), 5 * 60 * 1000);
}
}
/**
* 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(): MetaApiWebsocketClient.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(): MetaApiWebsocketClient.AccountsByReplica {
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
*/
private _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: string, replicas: MetatraderAccount.AccountsByRegion) {
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: string, replicas: MetatraderAccount.AccountsByRegion) {
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: string) {
if (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
*/
async lockSocketInstance(instanceNumber, socketInstanceIndex, region, metadata) {
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];
await socketInstance.socket.disconnect();
await 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
*/
async connect(instanceNumber, region) {
if (this._region && region !== this._region) {
throw new ValidationError(`Trying to connect to ${region} region, but configured with ${this._region}`);
}
let clientId = Math.random();
this._socketInstances[region] ||= {};
this._socketInstances[region][instanceNumber] ||= [];
const socketInstanceIndex = this._socketInstances[region][instanceNumber].length;
const instance: MetaApiWebsocketClient.SocketInstance = {
id: socketInstanceIndex,
reconnectWaitTime: this._socketMinimumReconnectTimeout,
connected: true,
requestResolves: {},
resolved: false,
connectResult: helpers.createHandlePromise<void>(),
sessionId: randomstring.generate(32),
isReconnecting: false,
socket: null,
synchronizationThrottler: new SynchronizationThrottler(this, socketInstanceIndex, instanceNumber, region,
this._synchronizationThrottlerOpts),
subscribeLock: null,
instanceNumber,
region
};
this._socketInstances[region][instanceNumber].push(instance);
instance.synchronizationThrottler.start();
instance.socket = this._createSocket(
await this._getServerUrl(instanceNumber, socketInstanceIndex, region), clientId, instance
);
return instance.connectResult;
}
/**
* Closes connection to MetaApi server
*/
close() {
Object.keys(this._socketInstances).forEach(region => {
Object.keys(this._socketInstances[region]).forEach(instanceNumber => {
this._socketInstances[region][instanceNumber].forEach(async (instance) => {
if (instance.connected) {
instance.connected = false;
await instance.socket.disconnect();
for (let requestResolve of Object.values<any>(instance.requestResolves)) {
requestResolve.reject(new Error('MetaApi connection closed'));
}
instance.requestResolves = {};
}
});
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
*/
async getAccountInformation(accountId, options?) {
let response = await this.rpcRequest(accountId, {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
*/
async getPositions(accountId, options?) {
let response = await this.rpcRequest(accountId, {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
*/
async getPosition(accountId, positionId, options?) {
let response = await this.rpcRequest(accountId, {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
*/
async getOrders(accountId, options?) {
let response = await this.rpcRequest(accountId, {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
*/
async getOrder(accountId, orderId, options?) {
let response = await this.rpcRequest(accountId, {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
*/
async getHistoryOrdersByTicket(accountId, ticket) {
let response = await 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
*/
async getHistoryOrdersByPosition(accountId, positionId) {
let response = await 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
*/
async getHistoryOrdersByTimeRange(accountId, startTime, endTime, offset = 0, limit = 1000) {
let response = await 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
*/
async getDealsByTicket(accountId, ticket) {
let response = await 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
*/
async getDealsByPosition(accountId, positionId) {
let response = await 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
*/
async getDealsByTimeRange(accountId, startTime, endTime, offset = 0, limit = 1000) {
let response = await 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
async trade(accountId, trade, application?, reliability?) {
let response;
if (application === 'RPC') {
response = await this.rpcRequest(accountId, {type: 'trade', trade, application});
} else {
response = await 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
*/
async synchronize(
accountId, instanceIndex, host, synchronizationId, startingHistoryOrderTime, startingDealTime, hashes
) {
if (this._getSocketInstanceByAccount(accountId, instanceIndex) === undefined) {
this._logger.debug(`${accountId}:${instanceIndex}: creating socket instance on synchronize`);
await 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
*/
async getSymbols(accountId) {
let response = await this.rpcRequest(accountId, {application: 'RPC', type: 'getSymbols'});
return response.symbols;
}
/**
* Retrieves specification for a symbol
* @param {String} accountId id of the MetaTrader account to retrieve symbol specification for
* @param {String} symbol symbol to retrieve specification for
* @returns {Promise<MetatraderSymbolSpecification>} promise which resolves when specification is retrieved
*/
async getSymbolSpecification(accountId, symbol) {
let response = await this.rpcRequest(accountId, {application: 'RPC', type: 'getSymbolSpecification', symbol});
return response.specification;
}
/**
* Retrieves price for a symbol
* @param {String} accountId id of the MetaTrader account to retrieve symbol price for
* @param {String} symbol symbol to retrieve price for
* @param {boolean} keepSubscription if set to true, the account will get a long-term subscription to symbol market
* data. Long-term subscription means that on subsequent calls you will get updated value faster. If set to false or
* not set, the subscription will be set to expire in 12 minutes.
* @returns {Promise<MetatraderSymbolPrice>} promise which resolves when price is retrieved
*/
async getSymbolPrice(accountId, symbol, keepSubscription = false) {
let response = await this.rpcRequest(accountId, {application: 'RPC', type: 'getSymbolPrice', symbol,
keepSubscription});
return response.price;
}
/**
* Retrieves price for a symbol
* @param {string} accountId id of the MetaTrader account to retrieve candle for
* @param {string} symbol symbol to retrieve candle for
* @param {string} timeframe defines the timeframe according to which the candle 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
* @param {boolean} keepSubscription if set to true, the account will get a long-term subscription to symbol market
* data. Long-term subscription means that on subsequent calls you will get updated value faster. If set to false or
* not set, the subscription will be set to expire in 12 minutes.
* @returns {Promise<MetatraderCandle>} promise which resolves when candle is retrieved
*/
async getCandle(accountId, symbol, timeframe, keepSubscription = false) {
let response = await this.rpcRequest(accountId, {application: 'RPC', type: 'getCandle', symbol, timeframe,
keepSubscription});
return response.candle;
}
/**
* Retrieves latest tick for a symbol. MT4 G1 accounts do not support this API
* @param {string} accountId id of the MetaTrader account to retrieve symbol tick for
* @param {string} symbol symbol to retrieve tick for
* @param {boolean} keepSubscription if set to true, the account will get a long-term subscription to symbol market
* data. Long-term subscription means that on subsequent calls you will get updated value faster. If set to false or
* not set, the subscription will be set to expire in 12 minutes.
* @returns {Promise<MetatraderTick>} promise which resolves when tick is retrieved
*/
async getTick(accountId, symbol, keepSubscription = false) {
let response = await this.rpcRequest(accountId, {application: 'RPC', type: 'getTick', symbol, keepSubscription});
return response.tick;
}
/**
* Retrieves latest order book for a symbol. MT4 accounts do not support this API
* @param {string} accountId id of the MetaTrader account to retrieve symbol order book for
* @param {string} symbol symbol to retrieve order book for
* @param {boolean} keepSubscription if set to true, the account will get a long-term subscription to symbol market
* data. Long-term subscription means that on subsequent calls you will get updated value faster. If set to false or
* not set, the subscription will be set to expire in 12 minutes.
* @returns {Promise<MetatraderBook>} promise which resolves when order book is retrieved
*/
async getBook(accountId, symbol, keepSubscription = false) {
let response = await this.rpcRequest(accountId, {application: 'RPC', type: 'getBook', symbol, keepSubscription});
return response.book;
}
/**
* Forces refresh of most recent quote updates for symbols subscribed to by the terminal
* @param {string} accountId id of the MetaTrader account
* @returns {Promise<string[]>} promise which resolves with recent quote symbols that was initiated to process
*/
async refreshTerminalState(accountId) {
let response = await this.rpcRequest(accountId, {application: 'RPC', type: 'refreshTerminalState'});
return response.symbols;
}
/**
* Forces refresh and retrieves latest quotes for a subset of symbols the terminal is subscribed to
* @param {string} accountId id of the MetaTrader account
* @param {string[]} symbols quote symbols to refresh
* @returns {Promise<RefreshedQuotes>} refreshed quotes and basic account information info
*/
async refreshSymbolQuotes(accountId, symbols) {
let response = await this.rpcRequest(accountId, {application: 'RPC', type: 'refreshSymbolQuotes', symbols});
return response.refreshedQuotes;
}
/**
* Sends client uptime stats to the server.
* @param {String} accountId id of the MetaTrader account to save uptime
* @param {Object} uptime uptime statistics to send to the server
* @returns {Promise} promise which resolves when uptime statistics is submitted
*/
saveUptime(accountId, uptime) {
return this.rpcRequest(accountId, {type: 'saveUptime', uptime});
}
/**
* Unsubscribe from account
* @param {String} accountId id of the MetaTrader account to unsubscribe
* @returns {Promise} promise which resolves when socket unsubscribed
*/
async unsubscribe(accountId) {
const region = this.getAccountRegion(accountId);
this._latencyService.onUnsubscribe(accountId);
Object.keys(this._updateEvents)
.filter(key => key.startsWith(accountId))
.forEach(key => delete this._updateEvents[key]);
if (this._socketInstances[region]) {
await Promise.all(Object.keys(this._socketInstances[region]).map(async instanceNumber => {
try {
await this._subscriptionManager.unsubscribe(accountId, Number(instanceNumber));
delete this._socketInstancesByAccounts[instanceNumber][accountId];
} catch (err) {
if (!(['TimeoutError', 'NotFoundError'].includes(err.name))) {
this._logger.warn(`${accountId}:${instanceNumber}: failed to unsubscribe`, err);
}
}
}));
}
}
/**
* Current server time (see https://metaapi.cloud/docs/client/models/serverTime/)
* @typedef {Object} ServerTime
* @property {Date} time current server time
* @property {String} brokerTime current broker time, in broker timezone, YYYY-MM-DD HH:mm:ss.SSS format
* @property {Date} [lastQuoteTime] last quote time
* @property {String} [lastQuoteBrokerTime] last quote time, in broker timezone, YYYY-MM-DD HH:mm:ss.SSS format
*/
/**
* Returns server time for a specified MetaTrader account
* @param {string} accountId id of the MetaTrader account to return server time for
* @returns {Promise<ServerTime>} promise resolving with server time
*/
async getServerTime(accountId) {
let response = await this.rpcRequest(accountId, {application: 'RPC', type: 'getServerTime'});
return response.serverTime;
}
/**
* Margin required to open a trade (see https://metaapi.cloud/docs/client/models/margin/)
* @typedef {Object} Margin
* @property {number} [margin] margin required to open a trade. If margin can not be calculated, then this field is
* not defined
*/
/**
* Contains order to calculate margin for (see https://metaapi.cloud/docs/client/models/marginOrder/)
* @typedef {Object} MarginOrder
* @property {string} symbol order symbol
* @property {string} type order type, one of ORDER_TYPE_BUY or ORDER_TYPE_SELL
* @property {number} volume order volume, must be greater than 0
* @property {number} openPrice order open price, must be greater than 0
*/
/**
* Calculates margin required to open a trade on the specified trading account
* @param {string} accountId id of the trading account to calculate margin for
* @param {string} application application to send the request to
* @param {string} reliability account reliability
* @param {MarginOrder} order order to calculate margin for
* @returns {Promise<Margin>} promise resolving with margin calculation result
*/
async calculateMargin(accountId, application, reliability, order) {
let response;
if (application === 'RPC') {
response = await this.rpcRequest(accountId, {application, type: 'calculateMargin', order});
} else {
response = await this.rpcRequestAllInstances(accountId, {application, type: 'calculateMargin', order},
reliability);
}
return response.margin;
}
/**
* Calls onUnsubscribeRegion listener event
* @param {String} accountId account id
* @param {String} region account region to unsubscribe
*/
async unsubscribeAccountRegion(accountId, region) {
const unsubscribePromises = [];
for (let listener of this._synchronizationListeners[accountId] || []) {
unsubscribePromises.push(this
._processEvent(() => listener.onUnsubscribeRegion(region), `${accountId}:${region}:onUnsubscribeRegion`, true)
.catch(err => this._logger.error(
`${accountId}:${region}: Failed to notify listener about onUnsubscribeRegion event`, err
))
);
}
await Promise.all(unsubscribePromises);
}
/**
* Adds synchronization listener for specific account
* @param {String} accountId account id
* @param {SynchronizationListener} listener synchronization listener to add
*/
addSynchronizationListener(accountId, listener) {
this._logger.trace(`${accountId}: Added synchronization listener`);
let listeners = this._synchronizationListeners[accountId];
if (!listeners) {
listeners = [];
this._synchronizationListeners[accountId] = listeners;
}
listeners.push(listener);
}
/**
* Removes synchronization listener for specific account
* @param {String} accountId account id
* @param {SynchronizationListener} listener synchronization listener to remove
*/
removeSynchronizationListener(accountId, listener) {
this._logger.trace(`${accountId}: Removed synchronization listener`);
let listeners = this._synchronizationListeners[accountId];
if (!listeners) {
listeners = [];
}
listeners = listeners.filter(l => l !== listener);
this._synchronizationListeners[accountId] = listeners;
}
/**
* Adds latency listener
* @param {LatencyListener} listener latency listener to add
*/
addLatencyListener(listener) {
this._latencyListeners.push(listener);
}
/**
* Removes latency listener
* @param {LatencyListener} listener latency listener to remove
*/
removeLatencyListener(listener) {
this._latencyListeners = this._latencyListeners.filter(l => l !== listener);
}
/**
* Adds reconnect listener
* @param {ReconnectListener} listener r