UNPKG

metaapi.cloud-sdk

Version:

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

1,386 lines (1,301 loc) 150 kB
'use strict'; import should from 'should'; import sinon from 'sinon'; import MetaApiWebsocketClient from './metaApiWebsocket.client'; import * as helpers from '../../helpers/helpers'; import Server from 'socket.io'; import log4js from 'log4js'; import {ForbiddenError} from '../errorHandler'; import MetaApi from '../../metaApi/metaApi'; import {AssertionError} from 'assert'; /** * @test {MetaApiWebsocketClient} */ // eslint-disable-next-line max-statements describe('MetaApiWebsocketClient', () => { const logger = log4js.getLogger('test'); let io; let server; let server1; let serverNewYork; let clock; let client: MetaApiWebsocketClient; let sandbox = sinon.createSandbox(); let activeSynchronizationIdsStub; const stubs = { latencyService: { onConnected: sinon.stub(), onDisconnected: sinon.stub(), onUnsubscribe: sinon.stub(), onDealsSynchronized: sinon.stub(), getActiveInstances: sinon.stub(), waitConnectedInstance: sinon.stub() } }; const metaApi = { _connectionRegistry: { rpcConnections: {}, streamingConnections: {} } }; const synchronizationThrottler = { activeSynchronizationIds: ['synchronizationId'], onDisconnect: () => {}, updateSynchronizationId: () => {}, removeSynchronizationId: () => {}, scheduleSynchronize: () => {} }; const domainClient = { getSettings: () => {} }; const options = { application: 'application', domain: 'project-stock.agiliumlabs.cloud', requestTimeout: 1.5, useSharedClientApi: true, disableInternalJobs: true, retryOpts: {retries: 3, minDelayInSeconds: 0.1, maxDelayInSeconds: 0.5}, region: undefined }; let accountInformation = { broker: 'True ECN Trading Ltd', currency: 'USD', server: 'ICMarketsSC-Demo', balance: 7319.9, equity: 7306.649913200001, margin: 184.1, freeMargin: 7120.22, leverage: 100, marginLevel: 3967.58283542 }; before(() => { MetaApi.enableLog4jsLogging(); log4js.configure(helpers.assembleLog4jsConfig()); }); beforeEach(async () => { clock = sinon.useFakeTimers({shouldAdvanceTime: true}); client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {...options}); client.url = 'http://localhost:6784'; (client as any)._socketInstances = {'vint-hill': {0: [], 1: []}, 'new-york': {0: []}}; io = new Server(6784, {path: '/ws', pingTimeout: 1000000}); io.on('connect', socket => { server = socket; if (socket.request._query['auth-token'] !== 'token') { socket.emit({error: 'UnauthorizedError', message: 'Authorization token invalid'}); socket.close(); } }); (client as any)._regionsByAccounts.accountId = {region: 'vint-hill', connections: 1}; (client as any)._regionsByAccounts.accountIdReplica = {region: 'new-york', connections: 1}; (client as any)._socketInstancesByAccounts = {0: {accountId: 0, accountIdReplica: 0}, 1: {accountId: 0}}; (client as any)._accountsByReplicaId.accountId = 'accountId'; (client as any)._accountsByReplicaId.accountIdReplica = 'accountId'; (client as any)._accountReplicas.accountId = { 'vint-hill': 'accountId', 'new-york': 'accountIdReplica' }; (client as any)._connectedHosts = { 'accountId:vint-hill:0:ps-mpa-1': 'ps-mpa-1', 'accountId:new-york:0:ps-mpa-2': 'ps-mpa-2' }; await client.connect(0, 'new-york'); serverNewYork = server; await client.connect(1, 'vint-hill'); server1 = server; server1.on('request', async data => { if (data.type === 'unsubscribe' && data.accountId === 'accountId') { server1.emit('response', {requestId: data.requestId, type: 'response', accountId: 'accountId'}); } }); await client.connect(0, 'vint-hill'); activeSynchronizationIdsStub = sandbox.stub( (client as any)._socketInstances['vint-hill'][0][0].synchronizationThrottler, 'activeSynchronizationIds' ).get(() => []); sandbox.stub( (client as any)._socketInstances['vint-hill'][1][0].synchronizationThrottler, 'activeSynchronizationIds' ).get(() => []); stubs.latencyService.onConnected = sandbox.stub((client as any)._latencyService, 'onConnected'); stubs.latencyService.onDisconnected = sandbox.stub((client as any)._latencyService, 'onDisconnected'); stubs.latencyService.onUnsubscribe = sandbox.stub((client as any)._latencyService, 'onUnsubscribe'); stubs.latencyService.onDealsSynchronized = sandbox.stub((client as any)._latencyService, 'onDealsSynchronized'); stubs.latencyService.waitConnectedInstance = sandbox.stub((client as any)._latencyService, 'waitConnectedInstance') .resolves('accountId:vint-hill:0:ps-mpa-1'); stubs.latencyService.getActiveInstances = sandbox.stub((client as any)._latencyService, 'getActiveAccountInstances') .returns([]); }); afterEach(async () => { clock.restore(); sandbox.restore(); let resolve; let promise = new Promise(res => resolve = res); client.close(); io.close(() => resolve()); await promise; }); /** * @test {MetaApiWebsocketClient#connect} */ describe('connect', () => { beforeEach(() => { client.close(); }); /** * @test {MetaApiWebsocketClient#connect} */ it('should throw validation error if connecting to region when configured another one', async () => { sandbox.stub(domainClient, 'getSettings').resolves({domain: 'v3.agiliumlabs.cloud'}); sandbox.stub(options, 'region').value('region1'); client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', options); client.url = 'http://localhost:6784'; try { await client.connect(0, 'region2'); throw new AssertionError({message: 'Should not be thrown'}); } catch (err) { err.name.should.equal('ValidationError'); logger.info(err); } }); /** * @test {MetaApiWebsocketClient#connect} */ it('should not throw error if connecting to same region as configured', async () => { sandbox.stub(domainClient, 'getSettings').resolves({domain: 'v3.agiliumlabs.cloud'}); sandbox.stub(options, 'region').value('region1'); client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', options); client.url = 'http://localhost:6784'; await client.connect(0, 'region1'); }); /** * @test {MetaApiWebsocketClient#connect} */ it('should connect to any region if it is not restrained in config', async () => { sandbox.stub(domainClient, 'getSettings').resolves({domain: 'v3.agiliumlabs.cloud'}); sandbox.stub(options, 'region').value(undefined); client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', options); client.url = 'http://localhost:6784'; await client.connect(0, 'region1'); await client.connect(0, 'region2'); }); }); /** * @test {MetaApiWebsocketClient#_tryReconnect} */ it('should change client id on reconnect', async () => { client.close(); let clientId; let connectAmount = 0; io.on('connect', socket => { connectAmount++; socket.request.headers['client-id'].should.equal(socket.request._query.clientId); socket.request.headers['client-id'].should.not.equal(clientId); socket.request._query.clientId.should.not.equal(clientId); clientId = socket.request._query.clientId; socket.disconnect(); }); await client.connect(0, 'vint-hill'); await new Promise(res => setTimeout(res, 50)); await clock.tickAsync(1500); await new Promise(res => setTimeout(res, 50)); connectAmount.should.be.aboveOrEqual(2); }); /** * @test {MetaApiWebsocketClient#connect} */ it('should retry connection if first attempt timed out', async () => { let positions = [{ id: '46214692', type: 'POSITION_TYPE_BUY', symbol: 'GBPUSD', magic: 1000, time: new Date('2020-04-15T02:45:06.521Z'), updateTime: new Date('2020-04-15T02:45:06.521Z'), openPrice: 1.26101, currentPrice: 1.24883, currentTickValue: 1, volume: 0.07, swap: 0, profit: -85.25999999999966, commission: -0.25, clientId: 'TE_GBPUSD_7hyINWqAlE', stopLoss: 1.17721, unrealizedProfit: -85.25999999999901, realizedProfit: -6.536993168992922e-13 }]; let resolve; let promise = new Promise(res => resolve = res); client.close(); io.close(() => resolve()); await promise; client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {application: 'application', domain: 'project-stock.agiliumlabs.cloud', requestTimeout: 1.5, useSharedClientApi: false, connectTimeout: 0.1, retryOpts: { retries: 3, minDelayInSeconds: 0.1, maxDelayInSeconds: 0.5}}); client.url = 'http://localhost:6785'; (async () => { await new Promise(res => setTimeout(res, 200)); io = new Server(6785, {path: '/ws', pingTimeout: 30000}); io.on('connect', socket => { server = socket; server.on('request', data => { if (data.type === 'getPositions' && data.accountId === 'accountId' && data.application === 'RPC') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, positions}); } }); }); })(); (client as any)._regionsByAccounts.accountId = {region: 'vint-hill', connections: 1}; (client as any)._accountsByReplicaId.accountId = 'accountId'; (client as any)._accountReplicas.accountId = { 'vint-hill': 'accountId' }; (client as any)._connectedHosts = { 'accountId:vint-hill:0:ps-mpa-1': 'ps-mpa-1' }; sandbox.stub((client as any)._latencyService, 'waitConnectedInstance').resolves('accountId:vint-hill:0:ps-mpa-1'); let actual = await client.getPositions('accountId'); actual.should.match(positions); io.close(); }); /** * @test {MetaApiWebsocketClient#connect} */ it('should wait for connected instance before sending requests', async () => { let positions = [{ id: '46214692', type: 'POSITION_TYPE_BUY', symbol: 'GBPUSD', magic: 1000, time: new Date('2020-04-15T02:45:06.521Z'), updateTime: new Date('2020-04-15T02:45:06.521Z'), openPrice: 1.26101, currentPrice: 1.24883, currentTickValue: 1, volume: 0.07, swap: 0, profit: -85.25999999999966, commission: -0.25, clientId: 'TE_GBPUSD_7hyINWqAlE', stopLoss: 1.17721, unrealizedProfit: -85.25999999999901, realizedProfit: -6.536993168992922e-13 }]; let resolve; let promise = new Promise(res => resolve = res); client.close(); io.close(() => resolve()); await promise; io = new Server(6785, {path: '/ws', pingTimeout: 1000000}); client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {application: 'application', domain: 'project-stock.agiliumlabs.cloud', requestTimeout: 1.5, useSharedClientApi: false, retryOpts: { retries: 3, minDelayInSeconds: 0.1, maxDelayInSeconds: 0.5}, eventProcessing: {sequentialProcessing: true}}); client.url = 'http://localhost:6785'; client.addAccountCache('accountId', {'vint-hill': 'accountId'}); sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true); io.on('connect', socket => { server = socket; if (socket.request._query['auth-token'] !== 'token') { socket.emit({error: 'UnauthorizedError', message: 'Authorization token invalid'}); socket.close(); } server.on('request', data => { if (data.type === 'getPositions' && data.accountId === 'accountId' && data.application === 'RPC') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, positions}); } else if (data.type === 'subscribe') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId}); } }); }); client.url = 'http://localhost:6785'; (client as any)._regionsByAccounts.accountId = {region: 'vint-hill', connections: 1}; (client as any)._regionsByAccounts.accountIdReplica = {region: 'new-york', connections: 1}; (client as any)._accountsByReplicaId.accountId = 'accountId'; (client as any)._accountReplicas.accountId = { 'vint-hill': 'accountId', }; (client as any)._connectedHosts = { 'accountId:vint-hill:0:ps-mpa-1': 'ps-mpa-1' }; server.on('request', data => { if (data.type === 'getPositions' && data.accountId === 'accountId' && data.application === 'RPC') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, positions}); } }); sandbox.stub((client as any)._latencyService, 'onConnected'); sandbox.stub((client as any)._latencyService, 'onDisconnected'); sandbox.stub((client as any)._latencyService, 'onUnsubscribe'); sandbox.stub((client as any)._latencyService, 'waitConnectedInstance').callsFake(async () => { await new Promise(res => setTimeout(res, 50)); return 'accountId:vint-hill:0:ps-mpa-1'; }); let actual = await client.getPositions('accountId'); actual.should.match(positions); io.close(); }); /** * @test {MetaApiWebsocketClient#_getServerUrl} */ it('should connect to shared server', async () => { client.close(); sandbox.stub(domainClient, 'getSettings').resolves({ domain: 'v3.agiliumlabs.cloud' }); client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {application: 'application', domain: 'project-stock.agiliumlabs.cloud', requestTimeout: 1.5, useSharedClientApi: true}); (client as any)._socketInstances = {'vint-hill': {0: [{ connected: true, requestResolves: [], socket: {close: () => {}} }]}}; const url = await (client as any)._getServerUrl(0, 0, 'vint-hill'); should(url).eql('https://mt-client-api-v1.vint-hill-a.v3.agiliumlabs.cloud'); }); /** * @test {MetaApiWebsocketClient#_getServerUrl} */ it('should connect to dedicated server', async () => { client.close(); sandbox.stub(domainClient, 'getSettings').resolves({ hostname: 'mt-client-api-dedicated', domain: 'project-stock.agiliumlabs.cloud' }); client = new MetaApiWebsocketClient(metaApi, domainClient, 'token', {application: 'application', domain: 'project-stock.agiliumlabs.cloud', requestTimeout: 1.5, useSharedClientApi: true}); (client as any)._socketInstances = {'vint-hill': {0: [{ connected: true, requestResolves: [], socket: {close: () => {}} }]}}; const url = await (client as any)._getServerUrl(0, 0, 'vint-hill'); should(url).eql('https://mt-client-api-v1.vint-hill-a.project-stock.agiliumlabs.cloud'); }); describe('addAccountCache', () => { /** * @test {MetaApiWebsocketClient#addAccountCache} */ it('should add account cache', async () => { client.addAccountCache('accountId2', {'vint-hill': 'accountId2'}); sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill'); sinon.assert.match(client.accountReplicas.accountId2, {'vint-hill': 'accountId2'}); sinon.assert.match(client.accountsByReplicaId.accountId2, 'accountId2'); client.addAccountCache('accountId2', {'vint-hill': 'accountId2'}); sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill'); client.removeAccountCache('accountId2'); sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill'); client.removeAccountCache('accountId2'); sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill'); for (let i = 0; i < 5; i++) { clock.tick(30 * 60 * 1000 + 500); client.clearAccountCacheJob(); } sinon.assert.match(client.getAccountRegion('accountId2'), undefined); sinon.assert.match(client.accountReplicas.accountId2, undefined); sinon.assert.match(client.accountsByReplicaId.accountId2, undefined); }); /** * @test {MetaApiWebsocketClient#addAccountCache} */ it('should delay region deletion if a request is made', async () => { server.on('request', data => { if (data.type === 'getAccountInformation' && data.accountId === 'accountId2' && data.application === 'RPC') { server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, accountInformation }); } }); client.addAccountCache('accountId2', {'vint-hill': 'accountId2'}); sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill'); await client.getAccountInformation('accountId2'); sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill'); await clock.tickAsync(30 * 60 * 1000 + 500); client.clearAccountCacheJob(); sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill'); await client.getAccountInformation('accountId2'); client.removeAccountCache('accountId2'); await clock.tickAsync(30 * 60 * 1000 + 500); client.clearAccountCacheJob(); await client.getAccountInformation('accountId2'); await clock.tickAsync(30 * 60 * 1000 + 500); client.clearAccountCacheJob(); sinon.assert.match(client.getAccountRegion('accountId2'), 'vint-hill'); for (let i = 0; i < 5; i++) { await clock.tickAsync(30 * 60 * 1000 + 500); client.clearAccountCacheJob(); } sinon.assert.match(client.getAccountRegion('accountId2'), undefined); }).timeout(3000); /** * @test {MetaApiWebsocketClient#addAccountCache} */ it('should correctly clear account cache including accounts with several replicas', () => { client.addAccountCache('accountId1', { 'vint-hill': 'accountId1', 'new-york': 'accountId2' }); client.removeAccountCache('accountId1'); clock.tick(1000 * 60 * 60 * 3); client.clearAccountCacheJob(); should(client.getAccountRegion('accountId1')).be.undefined(); should(client.getAccountRegion('accountId2')).be.undefined(); }); /** * @test {MetaApiWebsocketClient#addAccountCache} */ it('should correctly clear account cache added on synchroniation packet', async function() { this.retries(2); server.emit('synchronization', {type: 'keepalive', accountId: 'accountId1'}); await new Promise(res => setTimeout(res, 25)); clock.tick(1000 * 60 * 60 * 3); client.clearAccountCacheJob(); }); /** * @test {MetaApiWebsocketClient#addAccountCache} * @test {MetaApiWebsocketClient#updateAccountCache} */ it('should update account cache', async () => { client.addAccountCache('accountId1', { 'vint-hill': 'accountId1', 'new-york': 'accountId2', 'tokyo': 'accountId3', }); client.updateAccountCache('accountId1', { 'vint-hill': 'accountId1', 'tokyo': 'accountId4', 'singapore': 'accountId5' }); sinon.assert.match(client.accountReplicas.accountId1, { 'vint-hill': 'accountId1', 'tokyo': 'accountId4', 'singapore': 'accountId5'}); }); }); /** * @test {MetaApiWebsocketClient#onAccountDeleted} */ describe('onAccountDeleted', () => { let cancelAccountStub; beforeEach(() => { cancelAccountStub = sandbox.stub((client as any)._subscriptionManager, 'cancelAccount'); }); /** * @test {MetaApiWebsocketClient#onAccountDeleted} */ it('should delete master account', async () => { client.addAccountCache('accountId1', { 'vint-hill': 'accountId1', 'new-york': 'accountId2', 'tokyo': 'accountId3', }); client.onAccountDeleted('accountId1'); sinon.assert.calledWith(cancelAccountStub, 'accountId1'); sinon.assert.calledWith(stubs.latencyService.onUnsubscribe, 'accountId1'); should(client.getAccountRegion('accountId1')).be.undefined(); should(client.getAccountRegion('accountId2')).be.undefined(); should(client.getAccountRegion('accountId3')).be.undefined(); should(client.accountReplicas.accountId1).be.undefined(); }); /** * @test {MetaApiWebsocketClient#onAccountDeleted} */ it('should delete replica', async () => { client.addAccountCache('accountId1', { 'vint-hill': 'accountId1', 'new-york': 'accountId2', 'tokyo': 'accountId3', }); client.onAccountDeleted('accountId2'); sinon.assert.calledWith(cancelAccountStub, 'accountId2'); sinon.assert.calledWith(stubs.latencyService.onUnsubscribe, 'accountId2'); should(client.getAccountRegion('accountId1')).eql('vint-hill'); should(client.getAccountRegion('accountId2')).be.undefined(); should(client.getAccountRegion('accountId3')).eql('tokyo'); should(client.accountReplicas.accountId1).eql({ 'vint-hill': 'accountId1', 'tokyo': 'accountId3', }); }); }); /** * @test {MetaApiWebsocketClient#getAccountInformation} */ describe('getAccountInformation', () => { /** * @test {MetaApiWebsocketClient#getAccountInformation} */ it('should retrieve MetaTrader account information from API', async () => { server.on('request', data => { should(data.refreshTerminalState).be.undefined(); if (data.type === 'getAccountInformation' && data.accountId === 'accountId' && data.application === 'RPC') { server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, accountInformation }); } }); let actual = await client.getAccountInformation('accountId'); actual.should.match(accountInformation); }); /** * @test {MetaApiWebsocketClient#getAccountInformation} */ it('should set refresh terminal state flag if it is specified', async () => { server.on('request', data => { data.should.match({type: 'getAccountInformation', refreshTerminalState: true}); server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, accountInformation }); }); await client.getAccountInformation('accountId', {refreshTerminalState: true}); }); }); /** * @test {MetaApiWebsocketClient#getPositions} */ describe('getPositions', () => { /** * @test {MetaApiWebsocketClient#getPositions} */ it('should retrieve MetaTrader positions from API', async () => { let positions = [{ id: '46214692', type: 'POSITION_TYPE_BUY', symbol: 'GBPUSD', magic: 1000, time: new Date('2020-04-15T02:45:06.521Z'), updateTime: new Date('2020-04-15T02:45:06.521Z'), openPrice: 1.26101, currentPrice: 1.24883, currentTickValue: 1, volume: 0.07, swap: 0, profit: -85.25999999999966, commission: -0.25, clientId: 'TE_GBPUSD_7hyINWqAlE', stopLoss: 1.17721, unrealizedProfit: -85.25999999999901, realizedProfit: -6.536993168992922e-13 }]; server.on('request', data => { should(data.refreshTerminalState).be.undefined(); if (data.type === 'getPositions' && data.accountId === 'accountId' && data.application === 'RPC') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, positions}); } }); let actual = await client.getPositions('accountId'); actual.should.match(positions); }); /** * @test {MetaApiWebsocketClient#getPositions} */ it('should set refresh terminal state flag if it is specified', async () => { server.on('request', data => { data.should.match({type: 'getPositions', refreshTerminalState: true}); server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId}); }); await client.getPositions('accountId', {refreshTerminalState: true}); }); }); /** * @test {MetaApiWebsocketClient#getPosition} */ describe('getPosition', () => { /** * @test {MetaApiWebsocketClient#getPosition} */ it('should retrieve MetaTrader position from API by id', async () => { let position = { id: '46214692', type: 'POSITION_TYPE_BUY', symbol: 'GBPUSD', magic: 1000, time: new Date('2020-04-15T02:45:06.521Z'), updateTime: new Date('2020-04-15T02:45:06.521Z'), openPrice: 1.26101, currentPrice: 1.24883, currentTickValue: 1, volume: 0.07, swap: 0, profit: -85.25999999999966, commission: -0.25, clientId: 'TE_GBPUSD_7hyINWqAlE', stopLoss: 1.17721, unrealizedProfit: -85.25999999999901, realizedProfit: -6.536993168992922e-13 }; server.on('request', data => { should(data.refreshTerminalState).be.undefined(); if (data.type === 'getPosition' && data.accountId === 'accountId' && data.positionId === '46214692' && data.application === 'RPC') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, position}); } }); let actual = await client.getPosition('accountId', '46214692'); actual.should.match(position); }); /** * @test {MetaApiWebsocketClient#getPosition} */ it('should set refresh terminal state flag if it is specified', async () => { server.on('request', data => { data.should.match({type: 'getPosition', refreshTerminalState: true}); server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId}); }); await client.getPosition('accountId', '123', {refreshTerminalState: true}); }); }); /** * @test {MetaApiWebsocketClient#getOrders} */ describe('getOrders', () => { /** * @test {MetaApiWebsocketClient#getOrders} */ it('should retrieve MetaTrader orders from API', async () => { let orders = [{ id: '46871284', type: 'ORDER_TYPE_BUY_LIMIT', state: 'ORDER_STATE_PLACED', symbol: 'AUDNZD', magic: 123456, platform: 'mt5', time: new Date('2020-04-20T08:38:58.270Z'), openPrice: 1.03, currentPrice: 1.05206, volume: 0.01, currentVolume: 0.01, comment: 'COMMENT2' }]; server.on('request', data => { should(data.refreshTerminalState).be.undefined(); if (data.type === 'getOrders' && data.accountId === 'accountId' && data.application === 'RPC') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, orders}); } }); let actual = await client.getOrders('accountId'); actual.should.match(orders); }); /** * @test {MetaApiWebsocketClient#getOrders} */ it('should set refresh terminal state flag if it is specified', async () => { server.on('request', data => { data.should.match({type: 'getOrders', refreshTerminalState: true}); server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId}); }); await client.getOrders('accountId', {refreshTerminalState: true}); }); }); /** * @test {MetaApiWebsocketClient#getOrder} */ describe('getOrder', () => { /** * @test {MetaApiWebsocketClient#getOrder} */ it('should retrieve MetaTrader order from API by id', async () => { let order = { id: '46871284', type: 'ORDER_TYPE_BUY_LIMIT', state: 'ORDER_STATE_PLACED', symbol: 'AUDNZD', magic: 123456, platform: 'mt5', time: new Date('2020-04-20T08:38:58.270Z'), openPrice: 1.03, currentPrice: 1.05206, volume: 0.01, currentVolume: 0.01, comment: 'COMMENT2' }; server.on('request', data => { should(data.refreshTerminalState).be.undefined(); if (data.type === 'getOrder' && data.accountId === 'accountId' && data.orderId === '46871284' && data.application === 'RPC') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, order}); } }); let actual = await client.getOrder('accountId', '46871284'); actual.should.match(order); }); /** * @test {MetaApiWebsocketClient#getOrder} */ it('should set refresh terminal state flag if it is specified', async () => { server.on('request', data => { data.should.match({type: 'getOrder', refreshTerminalState: true}); server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId}); }); await client.getOrder('accountId', '123', {refreshTerminalState: true}); }); }); /** * @test {MetaApiWebsocketClient#getHistoryOrdersByTicket} */ it('should retrieve MetaTrader history orders from API by ticket', async () => { let historyOrders = [{ clientId: 'TE_GBPUSD_7hyINWqAlE', currentPrice: 1.261, currentVolume: 0, doneTime: new Date('2020-04-15T02:45:06.521Z'), id: '46214692', magic: 1000, platform: 'mt5', positionId: '46214692', state: 'ORDER_STATE_FILLED', symbol: 'GBPUSD', time: new Date('2020-04-15T02:45:06.260Z'), type: 'ORDER_TYPE_BUY', volume: 0.07 }]; server.on('request', data => { if (data.type === 'getHistoryOrdersByTicket' && data.accountId === 'accountId' && data.ticket === '46214692' && data.application === 'RPC') { server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, historyOrders, synchronizing: false }); } }); let actual = await client.getHistoryOrdersByTicket('accountId', '46214692'); actual.should.match({historyOrders, synchronizing: false}); }); /** * @test {MetaApiWebsocketClient#getHistoryOrdersByPosition} */ it('should retrieve MetaTrader history orders from API by position', async () => { let historyOrders = [{ clientId: 'TE_GBPUSD_7hyINWqAlE', currentPrice: 1.261, currentVolume: 0, doneTime: new Date('2020-04-15T02:45:06.521Z'), id: '46214692', magic: 1000, platform: 'mt5', positionId: '46214692', state: 'ORDER_STATE_FILLED', symbol: 'GBPUSD', time: new Date('2020-04-15T02:45:06.260Z'), type: 'ORDER_TYPE_BUY', volume: 0.07 }]; server.on('request', data => { if (data.type === 'getHistoryOrdersByPosition' && data.accountId === 'accountId' && data.positionId === '46214692' && data.application === 'RPC') { server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, historyOrders, synchronizing: false }); } }); let actual = await client.getHistoryOrdersByPosition('accountId', '46214692'); actual.should.match({historyOrders, synchronizing: false}); }); /** * @test {MetaApiWebsocketClient#getHistoryOrdersByTimeRange} */ it('should retrieve MetaTrader history orders from API by time range', async () => { let historyOrders = [{ clientId: 'TE_GBPUSD_7hyINWqAlE', currentPrice: 1.261, currentVolume: 0, doneTime: new Date('2020-04-15T02:45:06.521Z'), id: '46214692', magic: 1000, platform: 'mt5', positionId: '46214692', state: 'ORDER_STATE_FILLED', symbol: 'GBPUSD', time: new Date('2020-04-15T02:45:06.260Z'), type: 'ORDER_TYPE_BUY', volume: 0.07 }]; server.on('request', data => { if (data.type === 'getHistoryOrdersByTimeRange' && data.accountId === 'accountId' && data.startTime === '2020-04-15T02:45:00.000Z' && data.endTime === '2020-04-15T02:46:00.000Z' && data.offset === 1 && data.limit === 100 && data.application === 'RPC') { server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, historyOrders, synchronizing: false }); } }); let actual = await client.getHistoryOrdersByTimeRange('accountId', new Date('2020-04-15T02:45:00.000Z'), new Date('2020-04-15T02:46:00.000Z'), 1, 100); actual.should.match({historyOrders, synchronizing: false}); }); /** * @test {MetaApiWebsocketClient#getDealsByTicket} */ it('should retrieve MetaTrader deals from API by ticket', async () => { let deals = [{ clientId: 'TE_GBPUSD_7hyINWqAlE', commission: -0.25, entryType: 'DEAL_ENTRY_IN', id: '33230099', magic: 1000, platform: 'mt5', orderId: '46214692', positionId: '46214692', price: 1.26101, profit: 0, swap: 0, symbol: 'GBPUSD', time: new Date('2020-04-15T02:45:06.521Z'), type: 'DEAL_TYPE_BUY', volume: 0.07 }]; server.on('request', data => { if (data.type === 'getDealsByTicket' && data.accountId === 'accountId' && data.ticket === '46214692' && data.application === 'RPC') { server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, deals, synchronizing: false }); } }); let actual = await client.getDealsByTicket('accountId', '46214692'); actual.should.match({deals, synchronizing: false}); }); /** * @test {MetaApiWebsocketClient#getDealsByPosition} */ it('should retrieve MetaTrader deals from API by position', async () => { let deals = [{ clientId: 'TE_GBPUSD_7hyINWqAlE', commission: -0.25, entryType: 'DEAL_ENTRY_IN', id: '33230099', magic: 1000, platform: 'mt5', orderId: '46214692', positionId: '46214692', price: 1.26101, profit: 0, swap: 0, symbol: 'GBPUSD', time: new Date('2020-04-15T02:45:06.521Z'), type: 'DEAL_TYPE_BUY', volume: 0.07 }]; server.on('request', data => { if (data.type === 'getDealsByPosition' && data.accountId === 'accountId' && data.positionId === '46214692' && data.application === 'RPC') { server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, deals, synchronizing: false }); } }); let actual = await client.getDealsByPosition('accountId', '46214692'); actual.should.match({deals, synchronizing: false}); }); /** * @test {MetaApiWebsocketClient#getDealsByTimeRange} */ it('should retrieve MetaTrader deals from API by time range', async () => { let deals = [{ clientId: 'TE_GBPUSD_7hyINWqAlE', commission: -0.25, entryType: 'DEAL_ENTRY_IN', id: '33230099', magic: 1000, platform: 'mt5', orderId: '46214692', positionId: '46214692', price: 1.26101, profit: 0, swap: 0, symbol: 'GBPUSD', time: new Date('2020-04-15T02:45:06.521Z'), type: 'DEAL_TYPE_BUY', volume: 0.07 }]; server.on('request', data => { if (data.type === 'getDealsByTimeRange' && data.accountId === 'accountId' && data.startTime === '2020-04-15T02:45:00.000Z' && data.endTime === '2020-04-15T02:46:00.000Z' && data.offset === 1 && data.limit === 100 && data.application === 'RPC') { server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, deals, synchronizing: false }); } }); let actual = await client.getDealsByTimeRange('accountId', new Date('2020-04-15T02:45:00.000Z'), new Date('2020-04-15T02:46:00.000Z'), 1, 100); actual.should.match({deals, synchronizing: false}); }); /** * @test {MetaApiWebsocketClient#removeApplication} */ it('should remove application from API', async () => { let requestReceived = false; server.on('request', data => { if (data.type === 'removeApplication' && data.accountId === 'accountId' && data.application === 'application') { requestReceived = true; server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId}); } }); await client.removeApplication('accountId'); requestReceived.should.be.true(); }); /** * @test {MetaApiWebsocketClient#trade} */ it('should execute a trade via new API version', async () => { let trade = { actionType: 'ORDER_TYPE_SELL', symbol: 'AUDNZD', volume: 0.07 }; let response = { numericCode: 10009, stringCode: 'TRADE_RETCODE_DONE', message: 'Request completed', orderId: '46870472' }; sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true); server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1', instanceIndex: 0, replicas: 1}); await new Promise(res => setTimeout(res, 100)); let instanceIndex; server.on('request', data => { instanceIndex = data.instanceIndex; data.trade.should.match(trade); if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response}); } }); let actual = await client.trade('accountId', trade); actual.should.match(response); should.equal(instanceIndex, 0); }); /** * @test {MetaApiWebsocketClient#trade} */ it('should execute a trade via a replica account', async () => { stubs.latencyService.getActiveInstances.returns(['accountId:new-york:0:ps-mpa-2']); let trade = { actionType: 'ORDER_TYPE_SELL', symbol: 'AUDNZD', volume: 0.07 }; let response = { numericCode: 10009, stringCode: 'TRADE_RETCODE_DONE', message: 'Request completed', orderId: '46870472' }; sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true); serverNewYork.emit('synchronization', {type: 'authenticated', accountId: 'accountIdReplica', host: 'ps-mpa-2', instanceIndex: 0, replicas: 1}); await new Promise(res => setTimeout(res, 100)); let instanceIndex; serverNewYork.on('request', data => { instanceIndex = data.instanceIndex; data.trade.should.match(trade); if (data.type === 'trade' && data.accountId === 'accountIdReplica' && data.application === 'application') { serverNewYork.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response}); } }); let actual = await client.trade('accountId', trade); actual.should.match(response); should.equal(instanceIndex, 0); }); /** * @test {MetaApiWebsocketClient#trade} */ it('should execute an RPC trade', async () => { let trade = { actionType: 'ORDER_TYPE_SELL', symbol: 'AUDNZD', volume: 0.07 }; let response = { numericCode: 10009, stringCode: 'TRADE_RETCODE_DONE', message: 'Request completed', orderId: '46870472' }; sandbox.stub((client as any)._subscriptionManager, 'isSubscriptionActive').returns(true); server.emit('synchronization', {type: 'authenticated', accountId: 'accountId', host: 'ps-mpa-1', instanceIndex: 0, replicas: 1}); await new Promise(res => setTimeout(res, 50)); let instanceIndex; server.on('request', data => { instanceIndex = data.instanceIndex; data.trade.should.match(trade); if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'RPC') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response}); } }); let actual = await client.trade('accountId', trade, 'RPC'); actual.should.match(response); should.not.exist(instanceIndex); }); /** * @test {MetaApiWebsocketClient#trade} */ it('should execute a trade via API and receive trade error from old API version', async () => { let trade = { actionType: 'ORDER_TYPE_SELL', symbol: 'AUDNZD', volume: 0.07 }; let response = { error: 10006, description: 'TRADE_RETCODE_REJECT', message: 'Request rejected', orderId: '46870472' }; server.on('request', data => { data.trade.should.match(trade); if (data.type === 'trade' && data.accountId === 'accountId' && data.application === 'application') { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, response}); } }); try { await client.trade('accountId', trade); throw new AssertionError({message: 'Trade error expected'}); } catch (err) { err.message.should.equal('Request rejected'); err.name.should.equal('TradeError'); err.stringCode.should.equal('TRADE_RETCODE_REJECT'); err.numericCode.should.equal(10006); } }); /** * @test {MetaApiWebsocketClient#subscribe} */ it('should connect to MetaTrader terminal', async () => { let requestReceived = false; server.on('request', data => { if (data.type === 'subscribe' && data.accountId === 'accountId' && data.application === 'application' && data.instanceIndex === 0) { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId}); requestReceived = true; } }); await client.subscribe('accountId', 0); await new Promise(res => setTimeout(res, 50)); requestReceived.should.be.true(); }); /** * @test {MetaApiWebsocketClient#subscribe} */ it('should connect to MetaTrader terminal via a replica even if synced main', async () => { stubs.latencyService.getActiveInstances.returns(['accountId:vint-hill:0:ps-mpa-1']); let requestReceived = false; serverNewYork.on('request', data => { if (data.type === 'subscribe' && data.accountId === 'accountIdReplica' && data.application === 'application' && data.instanceIndex === 0) { serverNewYork.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId}); requestReceived = true; } }); await client.subscribe('accountIdReplica', 0); await new Promise(res => setTimeout(res, 50)); requestReceived.should.be.true(); }); /** * @test {MetaApiWebsocketClient#subscribe} */ it('should create new instance when account limit is reached', async () => { sinon.assert.match(client.socketInstances['vint-hill'][0].length, 1); for (let i = 0; i < 100; i++) { (client as any)._socketInstancesByAccounts[0]['accountId' + i] = 0; (client as any)._regionsByAccounts['accountId' + i] = {region: 'vint-hill', connections: 1}; } io.removeAllListeners('connect'); io.on('connect', socket => { socket.on('request', data => { if (data.type === 'subscribe' && data.accountId === 'accountId101' && data.application === 'application' && data.instanceIndex === 0) { socket.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId}); } }); }); (client as any)._regionsByAccounts.accountId101 = {region: 'vint-hill', connections: 1}; await client.subscribe('accountId101', 0); await new Promise(res => setTimeout(res, 50)); sinon.assert.match(client.socketInstances['vint-hill'][0].length, 2); }); /** * @test {MetaApiWebsocketClient#subscribe} */ it('should return error if connect to MetaTrader terminal failed', async () => { let requestReceived = false; server.on('request', data => { if (data.type === 'subscribe' && data.accountId === 'accountId' && data.application === 'application') { requestReceived = true; } server.emit('processingError', { id: 1, error: 'NotAuthenticatedError', message: 'Error message', requestId: data.requestId }); }); let success = true; try { await client.subscribe('accountId', 0); success = false; } catch (err) { err.name.should.equal('NotConnectedError'); } success.should.be.true(); requestReceived.should.be.true(); }); /** * @test {MetaApiWebsocketClient#getSymbols} */ it('should retrieve symbols from API', async () => { let symbols = ['EURUSD']; server.on('request', data => { if (data.type === 'getSymbols' && data.accountId === 'accountId' && data.application === 'RPC') { server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, symbols }); } }); let actual = await client.getSymbols('accountId'); actual.should.match(symbols); }); /** * @test {MetaApiWebsocketClient#getSymbolSpecification} */ it('should retrieve symbol specification from API', async () => { let specification = { symbol: 'AUDNZD', tickSize: 0.00001, minVolume: 0.01, maxVolume: 100, volumeStep: 0.01 }; server.on('request', data => { if (data.type === 'getSymbolSpecification' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' && data.application === 'RPC') { server.emit('response', { type: 'response', accountId: data.accountId, requestId: data.requestId, specification }); } }); let actual = await client.getSymbolSpecification('accountId', 'AUDNZD'); actual.should.match(specification); }); /** * @test {MetaApiWebsocketClient#getSymbolPrice} */ it('should retrieve symbol price from API', async () => { let price = { symbol: 'AUDNZD', bid: 1.05297, ask: 1.05309, profitTickValue: 0.59731, lossTickValue: 0.59736 }; server.on('request', data => { if (data.type === 'getSymbolPrice' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' && data.application === 'RPC' && data.keepSubscription === true) { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, price}); } }); let actual = await client.getSymbolPrice('accountId', 'AUDNZD', true); actual.should.match(price); }); /** * @test {MetaApiWebsocketClient#getCandle} */ it('should retrieve candle from API', async () => { let candle = { symbol: 'AUDNZD', timeframe: '15m', time: new Date('2020-04-07T03:45:00.000Z'), brokerTime: '2020-04-07 06:45:00.000', open: 1.03297, high: 1.06309, low: 1.02705, close: 1.043, tickVolume: 1435, spread: 17, volume: 345 }; server.on('request', data => { if (data.type === 'getCandle' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' && data.application === 'RPC' && data.timeframe === '15m' && data.keepSubscription === true) { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, candle}); } }); let actual = await client.getCandle('accountId', 'AUDNZD', '15m', true); actual.should.match(candle); }); /** * @test {MetaApiWebsocketClient#getTick} */ it('should retrieve latest tick from API', async () => { let tick = { symbol: 'AUDNZD', time: new Date('2020-04-07T03:45:00.000Z'), brokerTime: '2020-04-07 06:45:00.000', bid: 1.05297, ask: 1.05309, last: 0.5298, volume: 0.13, side: 'buy' }; server.on('request', data => { if (data.type === 'getTick' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' && data.application === 'RPC' && data.keepSubscription === true) { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, tick}); } }); let actual = await client.getTick('accountId', 'AUDNZD', true); actual.should.match(tick); }); /** * @test {MetaApiWebsocketClient#getBook} */ it('should retrieve latest order book from API', async () => { let book = { symbol: 'AUDNZD', time: new Date('2020-04-07T03:45:00.000Z'), brokerTime: '2020-04-07 06:45:00.000', book: [ { type: 'BOOK_TYPE_SELL', price: 1.05309, volume: 5.67 }, { type: 'BOOK_TYPE_BUY', price: 1.05297, volume: 3.45 } ] }; server.on('request', data => { if (data.type === 'getBook' && data.accountId === 'accountId' && data.symbol === 'AUDNZD' && data.application === 'RPC' && data.keepSubscription === true) { server.emit('response', {type: 'response', accountId: data.accountId, requestId: data.requestId, book}); } }); let actual = await client.getBook('accountId', 'AUDNZD', true); actual.should.match(book); }); /** * @test {MetaApiWebsocketClient#sendUptime} */ it('should sent uptime stats to the server', async () => { server.on('request', data => { if (data.type === 'saveUptime' && data.accountId === 'accountId' && JSON.stringify(data.uptime) === JSON.stringify({'1h': 100}) && data.application === 'application') { server.emit('response',