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)

707 lines (637 loc) 27.1 kB
'use strict'; import TimeoutError from '../clients/timeoutError'; import RpcMetaApiConnectionInstance from './rpcMetaApiConnectionInstance'; import StreamingMetaApiConnectionInstance from './streamingMetaApiConnectionInstance'; const HistoryDatabase = require('./historyDatabase/index').default; import ExpertAdvisor from './expertAdvisor'; import {ValidationError} from '../clients/errorHandler'; import MetatraderAccountReplica from './metatraderAccountReplica'; //eslint-disable-next-line max-len import MetatraderAccountClient, { Reliability, State, Version, ConnectionStatus, CopyFactoryRoles, Type, AccountConnection, ConfigurationLink, MetatraderAccountDto, DedicatedIp, NewMetaTraderAccountReplicaDto, MetatraderAccountUpdateDto } from '../clients/metaApi/metatraderAccount.client'; import type ConnectionRegistry from './connectionRegistry'; import type MetaApiWebsocketClient from '../clients/metaApi/metaApiWebsocket.client'; import type ExpertAdvisorClient from '../clients/metaApi/expertAdvisor.client'; import type HistoricalMarketDataClient from '../clients/metaApi/historicalMarketData.client'; import HistoryStorage from './historyStorage'; import {NewExpertAdvisorDto} from '../clients/metaApi/expertAdvisor.client'; import {MetatraderCandle, MetatraderTick} from '../clients/metaApi/metaApiWebsocket.client'; /** * Implements a MetaTrader account entity */ class MetatraderAccount { private _data: any; private _metatraderAccountClient: any; private _metaApiWebsocketClient: any; private _connectionRegistry: any; private _expertAdvisorClient: any; private _historicalMarketDataClient: any; private _application: any; private _replicas: any; /** * Constructs a MetaTrader account entity * @param {MetatraderAccountDto} data MetaTrader account data * @param {MetatraderAccountClient} metatraderAccountClient MetaTrader account REST API client * @param {MetaApiWebsocketClient} metaApiWebsocketClient MetaApi websocket client * @param {ConnectionRegistry} connectionRegistry metatrader account connection registry * @param {ExpertAdvisorClient} expertAdvisorClient expert advisor REST API client * @param {HistoricalMarketDataClient} historicalMarketDataClient historical market data HTTP API client * @param {string} application application name */ constructor( data: MetatraderAccountDto, metatraderAccountClient: MetatraderAccountClient, metaApiWebsocketClient: MetaApiWebsocketClient, connectionRegistry: ConnectionRegistry, expertAdvisorClient: ExpertAdvisorClient, historicalMarketDataClient: HistoricalMarketDataClient, application: string ) { this._data = data; this._metatraderAccountClient = metatraderAccountClient; this._metaApiWebsocketClient = metaApiWebsocketClient; this._connectionRegistry = connectionRegistry; this._expertAdvisorClient = expertAdvisorClient; this._historicalMarketDataClient = historicalMarketDataClient; this._application = application; this._replicas = (data.accountReplicas || []) .map(replica => new MetatraderAccountReplica(replica, this, metatraderAccountClient)); } /** * Returns unique account id * @return {string} unique account id */ get id(): string { return this._data._id; } /** * Returns current account state. One of CREATED, DEPLOYING, DEPLOYED, DEPLOY_FAILED, UNDEPLOYING, * UNDEPLOYED, UNDEPLOY_FAILED, DELETING, DELETE_FAILED, REDEPLOY_FAILED, DRAFT * @return {State} current account state */ get state(): State { return this._data.state; } /** * Returns MetaTrader magic to place trades using * @return {number} MetaTrader magic to place trades using */ get magic(): number { return this._data.magic; } /** * Returns terminal & broker connection status, one of CONNECTED, DISCONNECTED, DISCONNECTED_FROM_BROKER * @return {ConnectionStatus} terminal & broker connection status */ get connectionStatus(): ConnectionStatus { return this._data.connectionStatus; } /** * Returns quote streaming interval in seconds * @return {number} quote streaming interval in seconds */ get quoteStreamingIntervalInSeconds(): number { return this._data.quoteStreamingIntervalInSeconds; } /** * Returns symbol provided by broker * @return {string} any symbol provided by broker */ get symbol(): string { return this._data.symbol; } /** * Returns reliability value. Possible values are regular and high * @return {Reliability} account reliability value */ get reliability(): Reliability { return this._data.reliability; } /** * Returns user-defined account tags * @return {Array<string>} user-defined account tags */ get tags(): Array<string> { return this._data.tags; } /** * Returns extra information which can be stored together with your account * @return {Object} extra information which can be stored together with your account */ get metadata(): Object { return this._data.metadata; } /** * Returns number of resource slots to allocate to account. Allocating extra resource slots * results in better account performance under load which is useful for some applications. E.g. if you have many * accounts copying the same strategy via CopyFactory API, then you can increase resourceSlots to get a lower trade * copying latency. Please note that allocating extra resource slots is a paid option. Please note that high * reliability accounts use redundant infrastructure, so that each resource slot for a high reliability account * is billed as 2 standard resource slots. * @return {number} number of resource slots to allocate to account */ get resourceSlots(): number { return this._data.resourceSlots; } /** * Returns the number of CopyFactory 2 resource slots to allocate to account. * Allocating extra resource slots results in lower trade copying latency. Please note that allocating extra resource * slots is a paid option. Please also note that CopyFactory 2 uses redundant infrastructure so that * each CopyFactory resource slot is billed as 2 standard resource slots. You will be billed for CopyFactory 2 * resource slots only if you have added your account to CopyFactory 2 by specifying copyFactoryRoles field. * @return {number} number of CopyFactory 2 resource slots to allocate to account */ get copyFactoryResourceSlots(): number { return this._data.copyFactoryResourceSlots; } /** * Returns account region * @return {string} account region value */ get region(): string { return this._data.region; } /** * Returns the time account was created at, in ISO format * @returns {string} the time account was created at, in ISO format */ get createdAt(): Date { return new Date(this._data.createdAt); } /** * Returns human-readable account name * @return {string} human-readable account name */ get name(): string { return this._data.name; } /** * Returns flag indicating if trades should be placed as manual trades on this account * @return {boolean} flag indicating if trades should be placed as manual trades on this account */ get manualTrades(): boolean { return this._data.manualTrades; } /** * Returns default trade slippage in points * @return {number} default trade slippage in points */ get slippage(): number { return this._data.slippage; } /** * Returns id of the account's provisioning profile * @return {string} id of the account's provisioning profile */ get provisioningProfileId(): string { return this._data.provisioningProfileId; } /** * Returns MetaTrader account login * @return {string} MetaTrader account number */ get login(): string { return this._data.login; } /** * Returns MetaTrader server name to connect to * @return {string} MetaTrader server name to connect to */ get server(): string { return this._data.server; } /** * Returns account type. Possible values are cloud-g1, cloud-g2 * @return {Type} account type */ get type(): Type { return this._data.type; } /** * Returns MT version. Possible values are 4 and 5 * @return {Version} MT version */ get version(): Version { return this._data.version; } /** * Returns hash-code of the account * @return {number} hash-code of the account */ get hash(): number { return this._data.hash; } /** * Returns 3-character ISO currency code of the account base currency. The setting is to be used * for copy trading accounts which use national currencies only, such as some Brazilian brokers. You should not alter * this setting unless you understand what you are doing. * @return {string} 3-character ISO currency code of the account base currency */ get baseCurrency(): string { return this._data.baseCurrency; } /** * Returns account roles for CopyFactory2 application. Possible values are `PROVIDER` and `SUBSCRIBER` * @return {Array<CopyFactoryRoles>} account roles for CopyFactory2 application */ get copyFactoryRoles(): Array<CopyFactoryRoles> { return this._data.copyFactoryRoles; } /** * Returns flag indicating that risk management API is enabled on account * @return {boolean} flag indicating that risk management API is enabled on account */ get riskManagementApiEnabled(): boolean { return this._data.riskManagementApiEnabled; } /** * Returns flag indicating that MetaStats API is enabled on account * @return {boolean} flag indicating that MetaStats API is enabled on account */ get metastatsApiEnabled(): boolean { return this._data.metastatsApiEnabled; } /** * Returns configured dedicated IP protocol to connect to the trading account terminal * @return {DedicatedIp} */ get allocateDedicatedIp(): DedicatedIp { return this._data.allocateDedicatedIp; } /** * Returns active account connections * @return {Array<AccountConnection>} active account connections */ get connections(): Array<AccountConnection> { return this._data.connections; } /** * Returns flag indicating that account is primary * @return {boolean} flag indicating that account is primary */ get primaryReplica(): boolean { return this._data.primaryReplica; } /** * Returns user id * @return {string} user id */ get userId(): string { return this._data.userId; } /** * Returns primary account id * @return {string} primary account id */ get primaryAccountId(): string { return this._data.primaryAccountId; } /** * Returns account replicas from DTO * @return {MetatraderAccountReplica[]} account replicas from DTO */ get accountReplicas(): MetatraderAccountReplica[] { return this._data.accountReplicas; } /** * Returns account replica instances * @return {MetatraderAccountReplica[]} account replica instances */ get replicas(): MetatraderAccountReplica[] { return this._replicas; } /** * Returns a dictionary with account's available regions and replicas * @returns accounts by region */ get accountRegions(): MetatraderAccount.AccountsByRegion { const regions = {[this.region]: this.id}; this.replicas.forEach(replica => regions[replica.region] = replica.id); return regions; } /** * Reloads MetaTrader account from API * @return {Promise} promise resolving when MetaTrader account is updated */ async reload() { this._data = await this._metatraderAccountClient.getAccount(this.id); const updatedReplicaData = (this._data.accountReplicas || []); const regions = updatedReplicaData.map(replica => replica.region); const createdReplicaRegions = this._replicas.map(replica => replica.region); this._replicas = this._replicas.filter(replica => regions.includes(replica.region)); this._replicas.forEach(replica => { const updatedData = updatedReplicaData.find(replicaData => replicaData.region === replica.region); replica.updateData(updatedData); }); updatedReplicaData.forEach(replica => { if(!createdReplicaRegions.includes(replica.region)) { this._replicas.push(new MetatraderAccountReplica(replica, this, this._metatraderAccountClient)); } }); } /** * Removes a trading account and stops the API server serving the account. * The account state such as downloaded market data history will be removed as well when you remove the account. * @return {Promise} promise resolving when account is scheduled for deletion */ async remove() { this._connectionRegistry.closeAllInstances(this.id); await this._metatraderAccountClient.deleteAccount(this.id); const fileManager = HistoryDatabase.getInstance(); await fileManager.clear(this.id, this._application); if ((this.type as any) !== 'self-hosted') { try { await this.reload(); } catch (err) { if (err.name !== 'NotFoundError') { throw err; } } } } /** * Starts API server and trading terminal for trading account. * This request will be ignored if the account is already deployed. * @returns {Promise} promise resolving when account is scheduled for deployment */ async deploy() { await this._metatraderAccountClient.deployAccount(this.id); await this.reload(); } /** * Stops API server and trading terminal for trading account. * This request will be ignored if trading account is already undeployed * @returns {Promise} promise resolving when account is scheduled for undeployment */ async undeploy() { this._connectionRegistry.closeAllInstances(this.id); await this._metatraderAccountClient.undeployAccount(this.id); await this.reload(); } /** * Redeploys trading account. This is equivalent to undeploy immediately followed by deploy * @returns {Promise} promise resolving when account is scheduled for redeployment */ async redeploy() { await this._metatraderAccountClient.redeployAccount(this.id); await this.reload(); } /** * Increases trading account reliability in order to increase the expected account uptime. * The account will be temporary stopped to perform this action. * Note that increasing reliability is a paid option * @returns {Promise} promise resolving when account reliability is increased */ async increaseReliability() { await this._metatraderAccountClient.increaseReliability(this.id); await this.reload(); } /** * Enables risk management API for trading account. * The account will be temporary stopped to perform this action. * Note that risk management API is a paid option * @returns {Promise} promise resolving when account risk management is enabled */ async enableRiskManagementApi() { await this._metatraderAccountClient.enableRiskManagementApi(this.id); await this.reload(); } /** * Enables copy factory API for trading account. * The account will be temporary stopped to perform this action. * Note that copy factory API is a paid option (see * @param {Array<CopyFactoryRoles>} copyFactoryRoles copy factory roles * @param {number} copyFactoryResourceSlots copy factory resource slots * @return {Promise} promise resolving when account copy factory is enabled */ async enableCopyFactoryApi(copyFactoryRoles: Array<CopyFactoryRoles>, copyFactoryResourceSlots: number) { await this._metatraderAccountClient.enableCopyFactoryApi(this.id, copyFactoryRoles, copyFactoryResourceSlots); await this.reload(); } /** * Enables MetaStats API for trading account. * The account will be temporary stopped to perform this action. * Note that this is a paid option * @returns {Promise} promise resolving when account MetaStats API is enabled */ async enableMetaStatsApi() { await this._metatraderAccountClient.enableMetaStatsApi(this.id); await this.reload(); } /** * Waits until API server has finished deployment and account reached the DEPLOYED state * @param {number} timeoutInSeconds wait timeout in seconds, default is 5m * @param {number} intervalInMilliseconds interval between account reloads while waiting for a change, default is 1s * @return {Promise} promise which resolves when account is deployed * @throws {TimeoutError} if account have not reached the DEPLOYED state within timeout allowed */ async waitDeployed(timeoutInSeconds = 300, intervalInMilliseconds = 1000) { let startTime = Date.now(); await this.reload(); while (this.state !== 'DEPLOYED' && (startTime + timeoutInSeconds * 1000) > Date.now()) { await this._delay(intervalInMilliseconds); await this.reload(); } if (this.state !== 'DEPLOYED') { throw new TimeoutError('Timed out waiting for account ' + this.id + ' to be deployed'); } } /** * Waits until API server has finished undeployment and account reached the UNDEPLOYED state * @param {number} timeoutInSeconds wait timeout in seconds, default is 5m * @param {number} intervalInMilliseconds interval between account reloads while waiting for a change, default is 1s * @return {Promise} promise which resolves when account is deployed * @throws {TimeoutError} if account have not reached the UNDEPLOYED state within timeout allowed */ async waitUndeployed(timeoutInSeconds = 300, intervalInMilliseconds = 1000) { let startTime = Date.now(); await this.reload(); while (this.state !== 'UNDEPLOYED' && (startTime + timeoutInSeconds * 1000) > Date.now()) { await this._delay(intervalInMilliseconds); await this.reload(); } if (this.state !== 'UNDEPLOYED') { throw new TimeoutError('Timed out waiting for account ' + this.id + ' to be undeployed'); } } /** * Waits until account has been deleted * @param {number} timeoutInSeconds wait timeout in seconds, default is 5m * @param {number} intervalInMilliseconds interval between account reloads while waiting for a change, default is 1s * @return {Promise} promise which resolves when account is deleted * @throws {TimeoutError} if account was not deleted within timeout allowed */ async waitRemoved(timeoutInSeconds = 300, intervalInMilliseconds = 1000) { let startTime = Date.now(); try { await this.reload(); while (startTime + timeoutInSeconds * 1000 > Date.now()) { await this._delay(intervalInMilliseconds); await this.reload(); } throw new TimeoutError('Timed out waiting for account ' + this.id + ' to be deleted'); } catch (err) { if (err.name === 'NotFoundError') { return; } else { throw err; } } } /** * Waits until API server has connected to the terminal and terminal has connected to the broker * @param {number} timeoutInSeconds wait timeout in seconds, default is 5m * @param {number} intervalInMilliseconds interval between account reloads while waiting for a change, default is 1s * @return {Promise} promise which resolves when API server is connected to the broker * @throws {TimeoutError} if account have not connected to the broker within timeout allowed */ async waitConnected(timeoutInSeconds = 300, intervalInMilliseconds = 1000) { const checkConnected = () => { return [this.connectionStatus].concat(this.replicas.map(replica => replica.connectionStatus)).includes('CONNECTED'); }; let startTime = Date.now(); await this.reload(); while (!checkConnected() && (startTime + timeoutInSeconds * 1000) > Date.now()) { await this._delay(intervalInMilliseconds); await this.reload(); } if (!checkConnected()) { throw new TimeoutError('Timed out waiting for account ' + this.id + ' to connect to the broker'); } } /** * Connects to MetaApi. There is only one connection per account. Subsequent calls to this method will return the same connection. * @param {HistoryStorage} historyStorage optional history storage * @param {Date} [historyStartTime] history start time. Used for tests * @return {StreamingMetaApiConnectionInstance} MetaApi connection instance */ getStreamingConnection(historyStorage?: HistoryStorage, historyStartTime?: Date): StreamingMetaApiConnectionInstance { if(this._metaApiWebsocketClient.region && this._metaApiWebsocketClient.region !== this.region) { throw new ValidationError( `Account ${this.id} is not on specified region ${this._metaApiWebsocketClient.region}` ); } return this._connectionRegistry.connectStreaming(this, historyStorage, historyStartTime); } /** * Connects to MetaApi via RPC connection instance. * @returns {RpcMetaApiConnectionInstance} MetaApi connection instance */ getRPCConnection(): RpcMetaApiConnectionInstance { if(this._metaApiWebsocketClient.region && this._metaApiWebsocketClient.region !== this.region) { throw new ValidationError( `Account ${this.id} is not on specified region ${this._metaApiWebsocketClient.region}` ); } return this._connectionRegistry.connectRpc(this); } /** * Updates trading account. * Please redeploy the trading account in order for updated settings to take effect * @param {MetatraderAccountUpdateDto} account updated account information * @return {Promise} promise resolving when account is updated */ async update(account: MetatraderAccountUpdateDto) { await this._metatraderAccountClient.updateAccount(this.id, account); await this.reload(); } /** * Creates a trading account replica in a region different from trading account region and starts a cloud API server for it * @param {NewMetaTraderAccountDto} account MetaTrader account data * @return {Promise<MetatraderAccountReplica>} promise resolving with created MetaTrader account replica entity */ async createReplica(account: NewMetaTraderAccountReplicaDto): Promise<MetatraderAccountReplica> { await this._metatraderAccountClient.createAccountReplica(this.id, account); await this.reload(); return this._replicas.find(r => r.region === account.region); } /** * Retrieves expert advisor of current account * @returns {Promise<ExpertAdvisor[]>} promise resolving with an array of expert advisor entities */ async getExpertAdvisors(): Promise<ExpertAdvisor[]> { this._checkExpertAdvisorAllowed(); let expertAdvisors = await this._expertAdvisorClient.getExpertAdvisors(this.id); return expertAdvisors.map(e => new ExpertAdvisor(e, this.id, this._expertAdvisorClient)); } /** * Retrieves a expert advisor of current account by id * @param {String} expertId expert advisor id * @returns {Promise<ExpertAdvisor>} promise resolving with expert advisor entity */ async getExpertAdvisor(expertId: string): Promise<ExpertAdvisor> { this._checkExpertAdvisorAllowed(); let expertAdvisor = await this._expertAdvisorClient.getExpertAdvisor(this.id, expertId); return new ExpertAdvisor(expertAdvisor, this.id, this._expertAdvisorClient); } /** * Creates an expert advisor * @param {string} expertId expert advisor id * @param {NewExpertAdvisorDto} expert expert advisor data * @returns {Promise<ExpertAdvisor>} promise resolving with expert advisor entity */ async createExpertAdvisor(expertId: string, expert: NewExpertAdvisorDto): Promise<ExpertAdvisor> { this._checkExpertAdvisorAllowed(); await this._expertAdvisorClient.updateExpertAdvisor(this.id, expertId, expert); return this.getExpertAdvisor(expertId); } /** * Returns historical candles for a specific symbol and timeframe from the MetaTrader account. * See https://metaapi.cloud/docs/client/restApi/api/retrieveMarketData/readHistoricalCandles/ * @param {string} symbol symbol to retrieve candles for (e.g. a currency pair or an index) * @param {string} timeframe 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 * @param {Date} [startTime] time to start loading candles from. Note that candles are loaded in backwards direction, so * this should be the latest time. Leave empty to request latest candles. * @param {number} [limit] maximum number of candles to retrieve. Must be less or equal to 1000 * @return {Promise<Array<MetatraderCandle>>} promise resolving with historical candles downloaded */ getHistoricalCandles( symbol: string, timeframe: string, startTime?: Date, limit?: number ): Promise<Array<MetatraderCandle>> { return this._historicalMarketDataClient.getHistoricalCandles(this.id, this.region, symbol, timeframe, startTime, limit); } /** * Returns historical ticks for a specific symbol from the MetaTrader account. This API is not supported by MT4 * accounts. * See https://metaapi.cloud/docs/client/restApi/api/retrieveMarketData/readHistoricalTicks/ * @param {string} symbol symbol to retrieve ticks for (e.g. a currency pair or an index) * @param {Date} [startTime] time to start loading ticks from. Note that candles are loaded in forward direction, so * this should be the earliest time. Leave empty to request latest candles. * @param {number} [offset] number of ticks to skip (you can use it to avoid requesting ticks from previous request * twice) * @param {number} [limit] maximum number of ticks to retrieve. Must be less or equal to 1000 * @return {Promise<Array<MetatraderTick>>} promise resolving with historical ticks downloaded */ getHistoricalTicks( symbol: string, startTime?: Date, offset?: number, limit?: number ): Promise<Array<MetatraderTick>> { return this._historicalMarketDataClient.getHistoricalTicks(this.id, this.region, symbol, startTime, offset, limit); } /** * Generates trading account configuration link by account id. * @param {number} [ttlInDays] Lifetime of the link in days. Default is 7. * @return {Promise<ConfigurationLink>} promise resolving with configuration link */ async createConfigurationLink(ttlInDays?: number): Promise<ConfigurationLink> { const configurationLink = await this._metatraderAccountClient.createConfigurationLink(this.id, ttlInDays); return configurationLink; } private _checkExpertAdvisorAllowed() { if (this.version !== 4 || this.type !== 'cloud-g1') { throw new ValidationError('Custom expert advisor is available only for MT4 G1 accounts'); } } private _delay(timeoutInMilliseconds) { return new Promise(res => setTimeout(res, timeoutInMilliseconds)); } } namespace MetatraderAccount { /** Replica account IDs by region, including primary replica */ export type AccountsByRegion = {[region: string]: string}; } export default MetatraderAccount;