forked from backingate/node-mt4-zmq-bridge
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
330 lines (299 loc) · 14.7 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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
const requestQueueValue = 5 // helps to maintain zmq queue
const requestTimeoutValue = 15000 // time after which request receives a timeout
const requestEvents = new events.EventEmitter() // used to handle data from responses
// Check whether connections to the servers are established.
function requestCheckConnections(fn) {
if (!bridgeObject.reqConnected) {
fn(new Error("ZMQ REQ connection is closed."))
return false
} else if (!bridgeObject.pullConnected) {
fn(new Error("ZMQ PULL connection is closed."))
return false
} else {
return true
}
}
// 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()
if (requestCheckConnections(callback)) // check status of req/pull connections
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("|")}`) // send request message
}, requestQueueValue)
} else { // Promise Variant
return new Promise((resolve, reject) => {
if (requestCheckConnections(reject)) // check status of req/pull connections
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("|")}`) // send 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)
}
// 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 = true)
pullSocket.on('connect', () => bridgeObject.pullConnected = true)
reqSocket.on('disconnect', () => bridgeObject.reqConnected = false)
pullSocket.on('disconnect', () => bridgeObject.pullConnected = false)
// 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
}
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