mt4-zmq-bridge
Version:
Node.js and MetaTrader 4 communication bridge with ZeroMQ.
324 lines (291 loc) • 14.7 kB
JavaScript
const events = require('events')
const url = require('url')
const zmq = require('zeromq')
// Identificators for internal request operations.
const REQUEST_PING = 1
const REQUEST_TRADE_OPEN = 11
const REQUEST_TRADE_MODIFY = 12
const REQUEST_TRADE_DELETE = 13
const REQUEST_DELETE_ALL_PENDING_ORDERS = 21
const REQUEST_CLOSE_MARKET_ORDER = 22
const REQUEST_CLOSE_ALL_MARKET_ORDERS = 23
const REQUEST_RATES = 31
const REQUEST_ACCOUNT = 41
const REQUEST_ORDERS = 51
// Identificators for internal response operations.
const RESPONSE_OK = 0
const RESPONSE_FAILED = 1
// Identificators for internal unit types.
const UNIT_CONTRACTS = 0
const UNIT_CURRENCY = 1
// See: https://docs.mql4.com/constants/tradingconstants/orderproperties
const OP_BUY = 0
const OP_SELL = 1
const OP_BUYLIMIT = 2
const OP_SELLLIMIT = 3
const OP_BUYSTOP = 4
const OP_SELLSTOP = 5
// See: https://book.mql4.com/appendix/errors
const ERROR_CODES = {
// Error codes returned from a trade server or client terminal:
0: [ 'No error returned.', 'ERR_NO_ERROR' ],
1: [ 'No error returned', 'ERR_NO_RESULT' ],
2: [ 'Common error.', 'ERR_COMMON_ERROR' ],
3: [ 'Invalid trade parameters.', 'ERR_INVALID_TRADE_PARAMETERS' ],
4: [ 'Trade server is busy.', 'ERR_SERVER_BUSY' ],
5: [ 'Old version of the client terminal.', 'ERR_OLD_VERSION' ],
6: [ 'No connection with trade server.', 'ERR_NO_CONNECTION' ],
7: [ 'Not enough rights.', 'ERR_NOT_ENOUGH_RIGHTS' ],
8: [ 'Too frequent requests.', 'ERR_TOO_FREQUENT_REQUESTS' ],
9: [ 'Malfunctional trade operation.', 'ERR_MALFUNCTIONAL_TRADE' ],
64: [ 'Account disabled.', 'ERR_ACCOUNT_DISABLED' ],
65: [ 'Invalid account.', 'ERR_INVALID_ACCOUNT' ],
128: [ 'Trade timeout.', 'ERR_TRADE_TIMEOUT' ],
129: [ 'Invalid price.', 'ERR_INVALID_PRICE' ],
130: [ 'Invalid stops.', 'ERR_INVALID_STOPS' ],
131: [ 'Invalid trade volume.', 'ERR_INVALID_TRADE_VOLUME' ],
132: [ 'Market is closed.', 'ERR_MARKET_CLOSED' ],
133: [ 'Trade is disabled.', 'ERR_TRADE_DISABLED' ],
134: [ 'Not enough money.', 'ERR_NOT_ENOUGH_MONEY' ],
135: [ 'Price changed.', 'ERR_PRICE_CHANGED' ],
136: [ 'Off quotes.', 'ERR_OFF_QUOTES' ],
137: [ 'Broker is busy.', 'ERR_BROKER_BUSY' ],
138: [ 'Requote.', 'ERR_REQUOTE' ],
139: [ 'Order is locked.', 'ERR_ORDER_LOCKED' ],
140: [ 'Long positions only allowed.', 'ERR_LONG_POSITIONS_ONLY_ALLOWED' ],
141: [ 'Too many requests.', 'ERR_TOO_MANY_REQUESTS' ],
145: [ 'Modification denied because an order is too close to market.', 'ERR_TRADE_MODIFY_DENIED' ],
146: [ 'Trade context is busy.', 'ERR_TRADE_CONTEXT_BUSY' ],
147: [ 'Expirations are denied by broker.', 'ERR_TRADE_EXPIRATION_DENIED' ],
148: [ 'The amount of opened and pending orders has reached the limit set by a broker.', 'ERR_TRADE_TOO_MANY_ORDERS' ],
// MQL4 run time error codes:
4000: [ 'No error.', 'ERR_NO_MQLERROR' ],
4001: [ 'Wrong function pointer.', 'ERR_WRONG_FUNCTION_POINTER' ],
4002: [ 'Array index is out of range.', 'ERR_ARRAY_INDEX_OUT_OF_RANGE' ],
4003: [ 'No memory for function call stack.', 'ERR_NO_MEMORY_FOR_FUNCTION_CALL_STACK' ],
4004: [ 'Recursive stack overflow.', 'ERR_RECURSIVE_STACK_OVERFLOW' ],
4005: [ 'Not enough stack for parameter.', 'ERR_NOT_ENOUGH_STACK_FOR_PARAMETER' ],
4006: [ 'No memory for parameter string.', 'ERR_NO_MEMORY_FOR_PARAMETER_STRING' ],
4007: [ 'No memory for temp string.', 'ERR_NO_MEMORY_FOR_TEMP_STRING' ],
4008: [ 'Not initialized string.', 'ERR_NOT_INITIALIZED_STRING' ],
4009: [ 'Not initialized string in an array.', 'ERR_NOT_INITIALIZED_ARRAYSTRING' ],
4010: [ 'No memory for an array string.', 'ERR_NO_MEMORY_FOR_ARRAYSTRING' ],
4011: [ 'Too long string.', 'ERR_TOO_LONG_STRING' ],
4012: [ 'Remainder from zero divide.', 'ERR_REMAINDER_FROM_ZERO_DIVIDE' ],
4013: [ 'Zero divide.', 'ERR_ZERO_DIVIDE' ],
4014: [ 'Unknown command.', 'ERR_UNKNOWN_COMMAND' ],
4015: [ 'Wrong jump.', 'ERR_WRONG_JUMP' ],
4016: [ 'Not initialized array.', 'ERR_NOT_INITIALIZED_ARRAY' ],
4017: [ 'DLL calls are not allowed.', 'ERR_DLL_CALLS_NOT_ALLOWED' ],
4018: [ 'Cannot load library.', 'ERR_CANNOT_LOAD_LIBRARY' ],
4019: [ 'Cannot call function.', 'ERR_CANNOT_CALL_FUNCTION' ],
4020: [ 'EA function calls are not allowed.', 'ERR_EXTERNAL_EXPERT_CALLS_NOT_ALLOWED' ],
4021: [ 'Not enough memory for a string returned from a function.', 'ERR_NOT_ENOUGH_MEMORY_FOR_RETURNED_STRING' ],
4022: [ 'System is busy.', 'ERR_SYSTEM_BUSY' ],
4050: [ 'Invalid function parameters count.', 'ERR_INVALID_FUNCTION_PARAMETERS_COUNT' ],
4051: [ 'Invalid function parameter value.', 'ERR_INVALID_FUNCTION_PARAMETER_VALUE' ],
4052: [ 'String function internal error.', 'ERR_STRING_FUNCTION_INTERNAL_ERROR' ],
4053: [ 'Some array error.', 'ERR_SOME_ARRAY_ERROR' ],
4054: [ 'Incorrect series array using.', 'ERR_INCORRECT_SERIES_ARRAY_USING' ],
4055: [ 'Custom indicator error.', 'ERR_CUSTOM_INDICATOR_ERROR' ],
4056: [ 'Arrays are incompatible.', 'ERR_INCOMPATIBLE_ARRAYS' ],
4057: [ 'Global variables processing error.', 'ERR_GLOBAL_VARIABLES_PROCESSING_ERROR' ],
4058: [ 'Global variable not found.', 'ERR_GLOBAL_VARIABLE_NOT_FOUND' ],
4059: [ 'Function is not allowed in testing mode.', 'ERR_FUNCTION_NOT_ALLOWED_IN_TESTING_MODE' ],
4060: [ 'Function is not confirmed.', 'ERR_FUNCTION_NOT_CONFIRMED' ],
4061: [ 'Mail sending error.', 'ERR_SEND_MAIL_ERROR' ],
4062: [ 'String parameter expected.', 'ERR_STRING_PARAMETER_EXPECTED' ],
4063: [ 'Integer parameter expected.', 'ERR_INTEGER_PARAMETER_EXPECTED' ],
4064: [ 'Double parameter expected.', 'ERR_DOUBLE_PARAMETER_EXPECTED' ],
4065: [ 'Array as parameter expected.', 'ERR_ARRAY_AS_PARAMETER_EXPECTED' ],
4066: [ 'Requested history data in updating state.', 'ERR_HISTORY_WILL_UPDATED' ],
4067: [ 'Some error in trade operation execution.', 'ERR_TRADE_ERROR' ],
4099: [ 'End of a file.', 'ERR_END_OF_FILE' ],
4100: [ 'Some file error.', 'ERR_SOME_FILE_ERROR' ],
4101: [ 'Wrong file name.', 'ERR_WRONG_FILE_NAME' ],
4102: [ 'Too many opened files.', 'ERR_TOO_MANY_OPENED_FILES' ],
4103: [ 'Cannot open file.', 'ERR_CANNOT_OPEN_FILE' ],
4104: [ 'Incompatible access to a file.', 'ERR_INCOMPATIBLE_ACCESS_TO_FILE' ],
4105: [ 'No order selected.', 'ERR_NO_ORDER_SELECTED' ],
4106: [ 'Unknown symbol.', 'ERR_UNKNOWN_SYMBOL' ],
4107: [ 'Invalid price.', 'ERR_INVALID_PRICE_PARAM' ],
4108: [ 'Invalid ticket.', 'ERR_INVALID_TICKET' ],
4109: [ 'Trade is not allowed.', 'ERR_TRADE_NOT_ALLOWED' ],
4110: [ 'Longs are not allowed.', 'ERR_LONGS_NOT_ALLOWED' ],
4111: [ 'Shorts are not allowed.', 'ERR_SHORTS_NOT_ALLOWED' ],
4200: [ 'Object already exists.', 'ERR_OBJECT_ALREADY_EXISTS' ],
4201: [ 'Unknown object property.', 'ERR_UNKNOWN_OBJECT_PROPERTY' ],
4202: [ 'Object does not exist.', 'ERR_OBJECT_DOES_NOT_EXIST' ],
4203: [ 'Unknown object type.', 'ERR_UNKNOWN_OBJECT_TYPE' ],
4204: [ 'No object name.', 'ERR_NO_OBJECT_NAME' ],
4205: [ 'Object coordinates error.', 'ERR_OBJECT_COORDINATES_ERROR' ],
4206: [ 'No specified subwindow.', 'ERR_NO_SPECIFIED_SUBWINDOW' ],
4207: [ 'Some error in object operation.', 'ERR_SOME_OBJECT_ERROR' ]
}
module.exports.connect = (reqUrl, pullUrl) => {
if (!reqUrl || !url.parse(reqUrl).hostname) {
throw new Error('reqUrl invalid.')
} else if (!pullUrl || !url.parse(pullUrl).hostname) {
throw new Error('pullUrl invalid.')
}
let requestId = 1 // used to identify requests
let requestTimeoutValue = 15000 // time after which request receives a timeout
const requestQueueValue = 5 // helps to maintain zmq queue
const requestEvents = new events.EventEmitter() // used to handle data from responses
// Read and return response data for callback or Promise.
function requestCallback(timeout, callback, resolve, reject, err, res) {
clearTimeout(timeout)
if (callback) {
err ? callback(new Error(ERROR_CODES[err][0]), res) : callback(null, res)
} else {
err ? reject(new Error(ERROR_CODES[err][0])) : resolve(res)
}
}
// A request timeout callback.
function requestTimeoutCallback({ reqId, onceCallback, fn }) {
requestEvents.removeListener(reqId, onceCallback)
fn(new Error('ZMQ Request timeout.'))
}
/**
* Send request to ZMQ server and wait for the response.
* An number of arguments can be provided. Response can be delivered from Callback or Promise.
* To use Callback response, provide a function as the last argument. Otherwise, a Promise will be returned.
*/
function request() {
const args = Array.prototype.slice.call(arguments)
const reqId = requestId++
let timeoutObject, timeout, callback, onceCallback
if (typeof args[args.length - 1] === 'function') { // Callback Variant
callback = args.pop()
setTimeout(() => { // maintains queueing
timeoutObject = { reqId, onceCallback, fn: callback } // to use onceCallback reference at later point
timeout = setTimeout(requestTimeoutCallback.bind(null, timeoutObject), requestTimeoutValue)
onceCallback = requestCallback.bind(null, timeout, callback, null, null)
timeoutObject.onceCallback = onceCallback // add reference in case of timeout
requestEvents.once(reqId, onceCallback) // wait for response from pull server
reqSocket.send(`${reqId}|${args.join('|')}`) // queue request message
}, requestQueueValue)
} else { // Promise Variant
return new Promise((resolve, reject) => {
setTimeout(() => { // maintains queueing
timeoutObject = { reqId, onceCallback, fn: reject } // to use onceCallback reference at later point
timeout = setTimeout(requestTimeoutCallback.bind(null, timeoutObject), requestTimeoutValue)
onceCallback = requestCallback.bind(null, timeout, null, resolve, reject)
timeoutObject.onceCallback = onceCallback // add reference in case of timeout
requestEvents.once(reqId, onceCallback) // wait for response from pull server
reqSocket.send(`${reqId}|${args.join('|')}`) // queue request message
}, requestQueueValue)
})
}
}
/**
* Message with items separated by pipe "|" sign.
* First item is always the id, second is the status, then message data follows.
* In case of error, there is only one message data item containing error code.
*
* @param {String} message
* @returns id, err, msg
*/
function parseMessage(message) {
const split = message.toString().split('|')
const id = split.shift()
const status = +split[0]
let err, msg
if (status === RESPONSE_OK) {
err = null
msg = split.length === 2 && split[1] === '' ? true : split
if (msg !== true) {
msg.shift()
}
} else if (status === RESPONSE_FAILED) {
err = split[1]
msg = null
}
return [ id, err, msg ]
}
// Check whether we are waiting for response from pull server for specific id.
function isWaitingForResponse(id) {
return requestEvents.listenerCount(id) > 0
}
// An empty function.
function noop() {}
// Used for the first time when connecting to the server to potentially indicate connection problems.
function checkConnection(socket, url) {
let reqDelayedConnects = 0
function performCheck() {
reqDelayedConnects++
if (reqDelayedConnects === 2) {
socket.removeListener('connect', connectCallback)
socket.removeListener('connect_delay', performCheck)
console.warn(`METATRADER4 cannot connect with ${url}.`)
}
}
function connectCallback() {
socket.removeListener('connect_delay', performCheck)
}
socket.once('connect', connectCallback)
socket.on('connect_delay', performCheck)
}
function setRequestTimeoutValue(value) {
if (typeof value === 'number') {
requestTimeoutValue = value
}
}
// Create socket objects
const reqSocket = zmq.socket('req')
const pullSocket = zmq.socket('pull')
// Listen for message, then parse and broadcast it
reqSocket.on('message', (msg) => {
const message = parseMessage(msg)
bridgeObject.onReqMessage(message[0], message[1], message[2])
})
pullSocket.on('message', (msg) => {
const message = parseMessage(msg)
bridgeObject.onPullMessage(message[0], message[1], message[2])
requestEvents.emit(message[0], message[1], message[2])
})
// Listen for connect and disconnect events
reqSocket.on('connect', () => bridgeObject.reqConnected = !reqSocket.closed)
pullSocket.on('connect', () => bridgeObject.pullConnected = !pullSocket.closed)
reqSocket.on('disconnect', () => bridgeObject.reqConnected = reqSocket.closed)
pullSocket.on('disconnect', () => bridgeObject.pullConnected = pullSocket.closed)
// Wait for potential connection problems
checkConnection(reqSocket, reqUrl)
checkConnection(pullSocket, pullUrl)
// Try to open the connections to MT4 client and monitor them
reqSocket.monitor().connect(reqUrl)
pullSocket.monitor().connect(pullUrl)
const bridgeObject = {
request: request,
onReqMessage: noop,
onPullMessage: noop,
isWaitingForResponse: isWaitingForResponse,
reqConnected: false,
pullConnected: false,
reqSocket: reqSocket,
pullSocket: pullSocket,
setRequestTimeoutValue: setRequestTimeoutValue
}
return bridgeObject
}
module.exports.REQUEST_PING = REQUEST_PING
module.exports.REQUEST_TRADE_OPEN = REQUEST_TRADE_OPEN
module.exports.REQUEST_TRADE_MODIFY = REQUEST_TRADE_MODIFY
module.exports.REQUEST_TRADE_DELETE = REQUEST_TRADE_DELETE
module.exports.REQUEST_DELETE_ALL_PENDING_ORDERS = REQUEST_DELETE_ALL_PENDING_ORDERS
module.exports.REQUEST_CLOSE_MARKET_ORDER = REQUEST_CLOSE_MARKET_ORDER
module.exports.REQUEST_CLOSE_ALL_MARKET_ORDERS = REQUEST_CLOSE_ALL_MARKET_ORDERS
module.exports.REQUEST_RATES = REQUEST_RATES
module.exports.REQUEST_ACCOUNT = REQUEST_ACCOUNT
module.exports.REQUEST_ORDERS = REQUEST_ORDERS
module.exports.RESPONSE_OK = RESPONSE_OK
module.exports.RESPONSE_FAILED = RESPONSE_FAILED
module.exports.UNIT_CONTRACTS = UNIT_CONTRACTS
module.exports.UNIT_CURRENCY = UNIT_CURRENCY
module.exports.OP_BUY = OP_BUY
module.exports.OP_SELL = OP_SELL
module.exports.OP_BUYLIMIT = OP_BUYLIMIT
module.exports.OP_SELLLIMIT = OP_SELLLIMIT
module.exports.OP_BUYSTOP = OP_BUYSTOP
module.exports.OP_SELLSTOP = OP_SELLSTOP
module.exports.ERROR_CODES = ERROR_CODES