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
text/typescript
'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',