├── .gitattributes ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── diagram_1.png ├── docs ├── api.md └── dev.md ├── metatrader4 ├── MQL4 │ ├── Include │ │ └── json │ │ │ ├── JAson.mqh │ │ │ └── README.md │ └── Scripts │ │ └── ZeroMQ_Server.mq4 ├── config │ ├── metaeditor.ini │ └── zeromq_server_startup.template.ini ├── restart_mt4.bat └── templates │ └── Popular.tpl └── remote_deploy.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # crlf: Windows files 2 | *.mqh eol=crlf 3 | *.mq4 eol=crlf 4 | *.bat eol=crlf 5 | 6 | # lf: Linux shell scripts and git dotfiles 7 | *.sh eol=lf 8 | .gitattributes eol=lf 9 | .gitmodules eol=lf 10 | 11 | # binaries 12 | *.dll binary 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # generated from a template 2 | metatrader4/config/zeromq_server_startup.ini 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "mql-zmq"] 2 | path = mql-zmq 3 | url = https://github.com/dingmaotu/mql-zmq.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CoeJoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MetaTrader 4 Server 2 | Provides a remote interface and high-level API for MetaTrader 4 via ZeroMQ sockets.\ 3 | See [API](docs/api.md) for supported operations. 4 | 5 | ![Diagram 1](diagram_1.png) 6 | 7 | ## Installation 8 | 1. Copy [metatrader4](metatrader4) into your MetaTrader 4 profile directory, merging the 9 | folder contents. 10 | 1. Copy [mql-zmq/Include/](https://github.com/dingmaotu/mql-zmq/tree/f9bf8d94a34194a4fe79ae625779226900fe8657/Include) into the `MQL4/Include` subdirectory of your MetaTrader 4 profile directory, merging the 11 | folder contents. 12 | 1. Copy [mql-zmq/Library/MT4/](https://github.com/dingmaotu/mql-zmq/tree/f9bf8d94a34194a4fe79ae625779226900fe8657/Library/MT4) into the `MQL4/Libraries` subdirectory of your MetaTrader 4 profile directory, merging the 13 | folder contents. 14 | 1. Launch MetaEditor, open `MQL4/Scripts/ZeroMQ_Server.mq4` in your MetaTrader 4 profile directory and compile it. 15 | 1. Start MetaTrader 4 and add the `ZeroMQ Server` script to any chart (chart symbol does not matter). The server begins 16 | listening for client requests and responds synchronously. 17 | 18 | ## Configuration 19 | The default listening port is `TCP/28282` but is configurable in the script parameters popup in the MetaTrader terminal, 20 | along with other parameters such as socket timeouts. If Windows Defender Firewall is running, you must forward the port. 21 | 22 | ## Usage 23 | Typical client usage: 24 | 25 | 1. Create a ZeroMQ context and a `REQ` socket with appropriate send/receive timeouts and options. 26 | 1. Connect the `REQ` socket to the server's `REP` socket. 27 | 1. Perform the following sequence any number of times: 28 | 1. Construct an [API](docs/api.md) request. 29 | 1. Send the request to the `REQ` socket. 30 | 1. Receive a JSON-formatted string response from the `REQ` socket. 31 | 1. Close the socket connection and destroy the ZeroMQ context. 32 | 33 | It is recommended to use one of the following client libraries to abstract away these details: 34 | - [Python client](https://github.com/CoeJoder/metatrader4-client-python) 35 | - [Java client](https://github.com/CoeJoder/metatrader4-client-java) 36 | 37 | ## Limitations 38 | The `REQ-REP` socket connection enforces a strict request-response cycle and may deadlock if connection is lost. 39 | The client libraries use `ZMQ_REQ_RELAXED` and `ZMQ_REQ_CORRELATE` socket options to prevent this in most cases. 40 | If a response is dropped, the client may want to catch the exception and check whether or not the operation was 41 | successful. 42 | 43 | ## Development 44 | If you want to make changes to the server implementation, it's useful to setup a [local dev environment](docs/dev.md). 45 | -------------------------------------------------------------------------------- /diagram_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoeJoder/metatrader4-server/fc3ab36d6824eade1826c2a81ae06e78cf92006f/diagram_1.png -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | The transport format for requests and responses is JSON. 3 | 4 | Request objects contain an `action` string property, with any params as additional properties. 5 | Response objects contain a `response` property. 6 | 7 | If the action completed with caveats, the response will contain a `warning` property. 8 | If the action failed, the response will contain one or more of the following properties: `error_code`, `error_code_description`, `error_message`. 9 | See [MQL4 error codes](https://docs.mql4.com/constants/errorswarnings/errorcodes). 10 | 11 | # Actions 12 | #### Account 13 | - [GET_ACCOUNT_INFO](#GET_ACCOUNT_INFO) 14 | - [GET_ACCOUNT_INFO_INTEGER](#GET_ACCOUNT_INFO_INTEGER) 15 | - [GET_ACCOUNT_INFO_DOUBLE](#GET_ACCOUNT_INFO_DOUBLE) 16 | #### Market 17 | - [GET_SYMBOLS](#GET_SYMBOLS) 18 | - [GET_SYMBOL_INFO](#GET_SYMBOL_INFO) 19 | - [GET_SYMBOL_MARKET_INFO](#GET_SYMBOL_MARKET_INFO) 20 | - [GET_SYMBOL_INFO_INTEGER](#GET_SYMBOL_INFO_INTEGER) 21 | - [GET_SYMBOL_INFO_DOUBLE](#GET_SYMBOL_INFO_DOUBLE) 22 | - [GET_SYMBOL_INFO_STRING](#GET_SYMBOL_INFO_STRING) 23 | - [GET_SYMBOL_TICK](#GET_SYMBOL_TICK) 24 | - [GET_OHLCV](#GET_OHLCV) 25 | - [GET_SIGNALS](#GET_SIGNALS) 26 | - [GET_SIGNAL_INFO](#GET_SIGNAL_INFO) 27 | - [RUN_INDICATOR](#RUN_INDICATOR) 28 | #### Trading 29 | - [GET_ORDER](#GET_ORDER) 30 | - [GET_ORDERS](#GET_ORDERS) 31 | - [GET_HISTORICAL_ORDERS](#GET_HISTORICAL_ORDERS) 32 | - [DO_ORDER_SEND](#DO_ORDER_SEND) 33 | - [DO_ORDER_CLOSE](#DO_ORDER_CLOSE) 34 | - [DO_ORDER_DELETE](#DO_ORDER_DELETE) 35 | - [DO_ORDER_MODIFY](#DO_ORDER_MODIFY) 36 | 37 | --- 38 | 39 | ### GET_ACCOUNT_INFO 40 | Get static information about the account. 41 | 42 | #### Example 43 | ```json 44 | { 45 | "action": "GET_ACCOUNT_INFO" 46 | } 47 | ``` 48 | #### Response 49 | ```json 50 | { 51 | "response": { 52 | "login": 2102224685, 53 | "name": "my Acme account", 54 | "server": "Acme-Server3", 55 | "currency": "USD", 56 | "company": "Acme, Ltd." 57 | } 58 | } 59 | ``` 60 | 61 | ### GET_ACCOUNT_INFO_INTEGER 62 | Get an integer property of the account (see [AccountInfoInteger](https://docs.mql4.com/account/accountinfointeger)). 63 | 64 | #### Parameters 65 | - `property_name` - the [ENUM_ACCOUNT_INFO_INTEGER](https://docs.mql4.com/constants/environment_state/accountinformation#enum_account_info_integer) to lookup 66 | 67 | #### Example 68 | ```json 69 | { 70 | "action": "GET_ACCOUNT_INFO_INTEGER", 71 | "property_name": "ACCOUNT_LIMIT_ORDERS" 72 | } 73 | ``` 74 | #### Response 75 | ```json 76 | { 77 | "response": 1500.0 78 | } 79 | ``` 80 | 81 | ### GET_ACCOUNT_INFO_DOUBLE 82 | Get a double property of the account (see [AccountInfoDouble](https://docs.mql4.com/account/accountinfodouble)). 83 | 84 | #### Parameters 85 | - `property_name` - the [ENUM_ACCOUNT_INFO_DOUBLE](https://docs.mql4.com/constants/environment_state/accountinformation#enum_account_info_double) property to lookup 86 | 87 | #### Example 88 | ```json 89 | { 90 | "action": "GET_ACCOUNT_INFO_DOUBLE", 91 | "property_name": "ACCOUNT_BALANCE" 92 | } 93 | ``` 94 | #### Response 95 | ```json 96 | { 97 | "response": 5000.0 98 | } 99 | ``` 100 | 101 | ### GET_SYMBOLS 102 | Get a list of all market symbols available for trading. 103 | 104 | #### Example 105 | ```json 106 | { 107 | "action": "GET_SYMBOLS" 108 | } 109 | ``` 110 | #### Response 111 | ```json 112 | { 113 | "response": [ 114 | "AAPL", 115 | "ADSGn", 116 | "AIG", 117 | "AIRF.PA", 118 | "ALVG", 119 | "AMZN", 120 | "AUDCAD", 121 | "AUDCHF", 122 | ... 123 | ] 124 | } 125 | ``` 126 | 127 | ### GET_SYMBOL_INFO 128 | Get static information about the given market symbols. 129 | 130 | #### Parameters 131 | - `names` - a list of symbol names 132 | 133 | #### Example 134 | ```json 135 | { 136 | "action": "GET_SYMBOL_INFO", 137 | "names": [ 138 | "EURUSD", 139 | "USDJPY" 140 | ] 141 | } 142 | ``` 143 | #### Response 144 | ```json 145 | { 146 | "response": { 147 | "EURUSD": { 148 | "digits": 5, 149 | "name": "EURUSD", 150 | "point": 1e-05, 151 | "trade_contract_size": 100000.0, 152 | "trade_freeze_level": 0, 153 | "trade_stops_level": 0, 154 | "trade_tick_size": 1e-05, 155 | "trade_tick_value": 1.0, 156 | "volume_max": 1000.0, 157 | "volume_min": 0.01, 158 | "volume_step": 0.01 159 | }, 160 | "USDJPY": { 161 | "digits": 3, 162 | "name": "USDJPY", 163 | "point": 0.001, 164 | "trade_contract_size": 100000.0, 165 | "trade_freeze_level": 0, 166 | "trade_stops_level": 0, 167 | "trade_tick_size": 0.001, 168 | "trade_tick_value": 0.9499924, 169 | "volume_max": 1000.0, 170 | "volume_min": 0.01, 171 | "volume_step": 0.01 172 | } 173 | } 174 | } 175 | ``` 176 | 177 | ### GET_SYMBOL_MARKET_INFO 178 | Get market information about the given symbol (see [MarketInfo](https://docs.mql4.com/marketinformation/marketinfo)). 179 | 180 | #### Parameters 181 | - `symbol` - the market symbol 182 | - `property` - the [symbol property](https://docs.mql4.com/constants/environment_state/marketinfoconstants) to lookup 183 | 184 | #### Example 185 | ```json 186 | { 187 | "action": "GET_SYMBOL_MARKET_INFO", 188 | "symbol": "EURUSD", 189 | "property": "MODE_BID" 190 | } 191 | ``` 192 | #### Response 193 | ```json 194 | { 195 | "response": 1.13006 196 | } 197 | ``` 198 | 199 | ### GET_SYMBOL_INFO_INTEGER 200 | Get an integer property of a symbol (see [SymbolInfoInteger](https://docs.mql4.com/marketinformation/symbolinfointeger)). 201 | 202 | #### Parameters 203 | - `symbol` - the market symbol 204 | - `property_name` - the [ENUM_SYMBOL_INFO_INTEGER](https://docs.mql4.com/constants/environment_state/marketinfoconstants#enum_symbol_info_integer) to lookup 205 | 206 | #### Example 207 | ```json 208 | { 209 | "action": "GET_SYMBOL_INFO_INTEGER", 210 | "symbol": "EURUSD", 211 | "property_name": "SYMBOL_SELECT" 212 | } 213 | ``` 214 | #### Response 215 | ```json 216 | { 217 | "response": 1 218 | } 219 | ``` 220 | 221 | ### GET_SYMBOL_INFO_DOUBLE 222 | Get a double property of a symbol (see [SymbolInfoDouble](https://docs.mql4.com/marketinformation/symbolinfodouble)). 223 | 224 | #### Parameters 225 | - `symbol` - the market symbol 226 | - `property_name` - the [ENUM_SYMBOL_INFO_DOUBLE](https://docs.mql4.com/constants/environment_state/marketinfoconstants#enum_symbol_info_double) to lookup 227 | 228 | #### Example 229 | ```json 230 | { 231 | "action": "GET_SYMBOL_INFO_DOUBLE", 232 | "symbol": "EURUSD", 233 | "property_name": "SYMBOL_BID" 234 | } 235 | ``` 236 | #### Response 237 | ```json 238 | { 239 | "response": 1.13247 240 | } 241 | ``` 242 | 243 | ### GET_SYMBOL_INFO_STRING 244 | Get a string property of a symbol (see [SymbolInfoString](https://docs.mql4.com/marketinformation/symbolinfostring)). 245 | 246 | #### Parameters 247 | - `symbol` - the market symbol 248 | - `property_name` - the [ENUM_SYMBOL_INFO_STRING](https://docs.mql4.com/constants/environment_state/marketinfoconstants#enum_symbol_info_string) to lookup 249 | 250 | #### Example 251 | ```json 252 | { 253 | "action": "GET_SYMBOL_INFO_STRING", 254 | "symbol": "EURUSD", 255 | "property_name": "SYMBOL_CURRENCY_BASE" 256 | } 257 | ``` 258 | #### Response 259 | ```json 260 | { 261 | "response": "EUR" 262 | } 263 | ``` 264 | 265 | ### GET_SYMBOL_TICK 266 | Get the current prices of the given symbol (see [SymbolInfoTick](https://docs.mql4.com/marketinformation/symbolinfotick)). 267 | 268 | #### Parameters 269 | - `symbol` - the market symbol 270 | 271 | #### Example 272 | ```json 273 | { 274 | "action": "GET_SYMBOL_TICK", 275 | "symbol": "EURUSD" 276 | } 277 | ``` 278 | #### Response 279 | ```json 280 | { 281 | "response": { 282 | "ask": 1.13346, 283 | "bid": 1.13338, 284 | "last": 0.0, 285 | "time": 1591744136, 286 | "volume": 0 287 | } 288 | } 289 | ``` 290 | 291 | ### GET_OHLCV 292 | Get the most recent OHLCV bars for the given symbol. 293 | 294 | #### Parameters 295 | - `symbol` - the market symbol 296 | - `timeframe` - the width of the bars, in minutes. 297 | Use a [standard timeframe](https://docs.mql4.com/constants/chartconstants/enum_timeframes) for a better chance of the broker's server responding successfully. 298 | - `limit` - the maximum number of bars to return 299 | - `timeout` - the timeout in milliseconds to wait for the broker's server to return the data 300 | 301 | #### Example 302 | ```json 303 | { 304 | "action": "GET_OHLCV", 305 | "symbol": "EURUSD", 306 | "timeframe": 60, 307 | "limit": 100, 308 | "timeout": 5000 309 | } 310 | ``` 311 | #### Response 312 | ```json 313 | { 314 | "response": [ 315 | { 316 | "open": 1.12307, 317 | "high": 1.124, 318 | "low": 1.12301, 319 | "close": 1.12372, 320 | "tick_volume": 1702, 321 | "time": 1592424000 322 | }, 323 | { 324 | "open": 1.12372, 325 | "high": 1.12372, 326 | "low": 1.12294, 327 | "close": 1.12308, 328 | "tick_volume": 1468, 329 | "time": 1592427600 330 | }, 331 | ... 332 | ] 333 | } 334 | ``` 335 | 336 | ### GET_SIGNALS 337 | Get a list of all trade signals available in the terminal. 338 | 339 | #### Example 340 | ```json 341 | { 342 | "action": "GET_SIGNALS" 343 | } 344 | ``` 345 | #### Response 346 | ```json 347 | { 348 | "response": [ 349 | "FibonacciBreakout", 350 | "EA Happy Gold", 351 | "Batman EA", 352 | "EA Happy Market Hours", 353 | "EA Red Dragon l Demo FXCC l", 354 | "EA Skynet l Demo FXCC l", 355 | ... 356 | } 357 | ``` 358 | 359 | ### GET_SIGNAL_INFO 360 | Get detailed information on one or more trading signals. 361 | 362 | #### Parameters 363 | - `names` - a list of signal names 364 | 365 | #### Example 366 | ```json 367 | { 368 | "action": "GET_SIGNAL_INFO", 369 | "names": [ 370 | "EA Happy Gold", 371 | "FibonacciBreakout" 372 | ] 373 | } 374 | ``` 375 | #### Response 376 | ```json 377 | { 378 | "response": { 379 | "EA Happy Gold": { 380 | "author_login": "HappyForex", 381 | "balance": 220230.23, 382 | "broker": "TF Global Markets (Aust) Pty Ltd", 383 | "broker_server": "ThinkForexAU-Demo", 384 | "currency": "USD", 385 | "date_published": 1396947432, 386 | "date_started": 1396940232, 387 | "equity": 220230.23, 388 | "gain": 21923.02, 389 | "id": 36094, 390 | "leverage": 400, 391 | "max_drawdown": 21.98, 392 | "name": "EA Happy Gold", 393 | "pips": 41114, 394 | "price": 20.0, 395 | "rating": 46, 396 | "roi": 21923.02, 397 | "subscribers": 0, 398 | "trade_mode": 1, 399 | "trades": 1443 400 | }, 401 | "FibonacciBreakout": { 402 | "author_login": "Antonov-EA", 403 | "balance": 21522.68, 404 | ... 405 | } 406 | } 407 | } 408 | ``` 409 | 410 | ### RUN_INDICATOR 411 | Run one of the built-in [technical indicator functions](https://docs.mql4.com/indicators). 412 | If data is requested that is not already loaded in the terminal, it must be loaded from the broker server, which is why 413 | a timeout must be specified. 414 | 415 | #### Parameters 416 | - `indicator` - the name of the indicator function 417 | - `argv` - a list of indicator function parameters 418 | - `timeout` - the timeout in milliseconds 419 | 420 | #### Example 421 | ```json 422 | { 423 | "action": "RUN_INDICATOR", 424 | "indicator": "iAC", 425 | "argv": ["EURUSD", 60, 1], 426 | "timeout": 5000 427 | } 428 | ``` 429 | #### Response 430 | ```json 431 | { 432 | "response": 2.251e-05 433 | } 434 | ``` 435 | 436 | ### GET_ORDER 437 | Lookup an open, pending, or closed order using its ticket number. 438 | 439 | #### Parameters 440 | - `ticket` - the ticket number of the order 441 | 442 | #### Example 443 | ```json 444 | { 445 | "action": "GET_ORDER", 446 | "ticket": 121204376 447 | } 448 | ``` 449 | #### Response 450 | ```json 451 | { 452 | "response": { 453 | "close_price": 1.13668, 454 | "close_time": "1970.01.01 00:00:00", 455 | "comment": null, 456 | "commission": -0.04, 457 | "expiration": "1970.01.01 00:00:00", 458 | "lots": 0.01, 459 | "magic_number": 0, 460 | "open_price": 1.13672, 461 | "open_time": "2020.06.10 23:30:32", 462 | "order_type": 0, 463 | "profit": -0.04, 464 | "sl": 0.0, 465 | "swap": 0.0, 466 | "symbol": "EURUSD", 467 | "ticket": 121204376, 468 | "tp": 0.0 469 | } 470 | } 471 | ``` 472 | 473 | ### GET_ORDERS 474 | Lookup all market and pending orders. 475 | 476 | #### Example 477 | ```json 478 | { 479 | "action": "GET_ORDERS" 480 | } 481 | ``` 482 | #### Response 483 | ```json 484 | { 485 | "response": [ 486 | { 487 | "close_price": 1.13668, 488 | "close_time": "1970.01.01 00:00:00", 489 | "comment": null, 490 | "commission": -0.04, 491 | "expiration": "1970.01.01 00:00:00", 492 | "lots": 0.01, 493 | "magic_number": 0, 494 | "open_price": 1.13672, 495 | "open_time": "2020.06.10 23:30:32", 496 | "order_type": 0, 497 | "profit": -0.04, 498 | "sl": 0.0, 499 | "swap": 0.0, 500 | "symbol": "EURUSD", 501 | "ticket": 121204376, 502 | "tp": 0.0 503 | }, { 504 | "close_price": 1.13671, 505 | ... 506 | } 507 | ] 508 | } 509 | ``` 510 | 511 | ### GET_HISTORICAL_ORDERS 512 | Lookup all closed orders loaded in the "Account History" tab of the MT4 terminal. 513 | 514 | #### Example 515 | ```json 516 | { 517 | "action": "GET_HISTORICAL_ORDERS" 518 | } 519 | ``` 520 | #### Response 521 | ```json 522 | { 523 | "response": [ 524 | { 525 | "close_price": 1.13672, 526 | "close_time": "2020.06.10 23:29:57", 527 | "comment": null, 528 | "commission": -0.04, 529 | "expiration": "1970.01.01 00:00:00", 530 | "lots": 0.01, 531 | "magic_number": 0, 532 | "open_price": 1.13665, 533 | "open_time": "2020.06.10 23:29:42", 534 | "order_type": 1, 535 | "profit": -0.07, 536 | "sl": 0.0, 537 | "swap": 0.0, 538 | "symbol": "EURUSD", 539 | "ticket": 121204101, 540 | "tp": 0.0 541 | }, 542 | { 543 | "close_price": 0.0, 544 | ... 545 | } 546 | ] 547 | } 548 | ``` 549 | 550 | ### DO_ORDER_SEND 551 | Open a market order or place a pending order. 552 | This action is more than just a wrapper around the [OrderSend()](https://docs.mql4.com/trading/ordersend) function. 553 | If stop-loss or take-profit params are specified, the order is opened with the given price and then modified with those values (in order to comply with the ECN trading protocol). 554 | If the values violate the [trading requirements and limitations](https://book.mql4.com/appendix/limits), they are nudged the minimum amount to allow the modification to take place. 555 | The resulting order is returned. 556 | 557 | #### Parameters 558 | - `symbol` - the market symbol 559 | - `order_type` - the order type. 560 | Must be an integer value from the [order properties](https://docs.mql4.com/constants/tradingconstants/orderproperties) table 561 | - `lots` - the number of lots to trade 562 | - `price` - the desired open price. Required for pending orders, but can be omitted for market orders 563 | - `slippage` - the maximum price slippage, in points. Omit to use a permissive default (2x the market spread) 564 | - `sl` - the absolute stop-loss 565 | - `sl_points` - the relative stop-loss (mutually exclusive with `sl`) 566 | - `tp` - the absolute take-profit 567 | - `tp_points` - the relative take-profit (mutually exclusive with `tp`) 568 | - `comment` - a string comment to attach to the order 569 | 570 | 571 | #### Example 572 | ```json 573 | { 574 | "action": "DO_ORDER_SEND", 575 | "symbol": "EURUSD", 576 | "order_type": 0, 577 | "lots": 1, 578 | "comment": "This is a market buy order." 579 | } 580 | ``` 581 | #### Response 582 | ```json 583 | { 584 | "response": { 585 | "close_price": 1.12523, 586 | "close_time": "1970.01.01 00:00:00", 587 | "comment": "This is a market buy order.", 588 | "commission": -4.0, 589 | "expiration": "1970.01.01 00:00:00", 590 | "lots": 1.0, 591 | "magic_number": 0, 592 | "open_price": 1.1253, 593 | "open_time": "2020.06.25 04:50:17", 594 | "order_type": 0, 595 | "profit": -7.0, 596 | "sl": 0.0, 597 | "swap": 0.0, 598 | "symbol": "EURUSD", 599 | "ticket": 124114600, 600 | "tp": 0.0 601 | } 602 | } 603 | ``` 604 | 605 | ### DO_ORDER_CLOSE 606 | Close an opened order. 607 | 608 | #### Parameters 609 | - `ticket` - the ticket number of the order 610 | - `lots` - the number of lots. Omit to close entire order 611 | - `price` - the closing price. Omit to close at market price 612 | - `slippage` - the maximum price slippage, in points. Omit to use a permissive default (2x the market spread) 613 | 614 | #### Example 615 | ```json 616 | { 617 | "action": "DO_ORDER_CLOSE", 618 | "ticket": 124114600 619 | } 620 | ``` 621 | #### Response 622 | ```json 623 | { 624 | "response": "Closed order # 124114600" 625 | } 626 | ``` 627 | 628 | ### DO_ORDER_DELETE 629 | Delete a pending order. 630 | 631 | #### Parameters 632 | - `ticket` - the ticket number of the order 633 | - `closeIfOpened` - if true and the order is open, it is closed at market price. Defaults to `false`. 634 | 635 | #### Example 636 | ```json 637 | { 638 | "action": "DO_ORDER_DELETE", 639 | "ticket": 124114600 640 | } 641 | ``` 642 | #### Response 643 | ```json 644 | { 645 | "response": "Deleted pending order # 124114600" 646 | } 647 | ``` 648 | 649 | ### DO_ORDER_MODIFY 650 | Modify a previously opened or pending order. 651 | 652 | #### Parameters 653 | - `ticket` - the ticket number of the order 654 | - `price` - the new open price of the pending order 655 | - `sl` - the absolute stop-loss 656 | - `sl_points` - the relative stop-loss (mutually exclusive with `sl`) 657 | - `tp` - the absolute take-profit 658 | - `tp_points` - the relative take-profit (mutually exclusive with `tp`) 659 | 660 | #### Example 661 | ```json 662 | { 663 | "action": "DO_ORDER_MODIFY", 664 | "ticket": 124114600, 665 | "price": 1.13552 666 | } 667 | ``` 668 | #### Response 669 | ```json 670 | { 671 | "response": { 672 | "close_price": 1.13552, 673 | "close_time": "1970.01.01 00:00:00", 674 | "comment": "This is a pending order.", 675 | "commission": -4.0, 676 | "expiration": "1970.01.01 00:00:00", 677 | "lots": 1.0, 678 | "magic_number": 0, 679 | "open_price": 1.13552, 680 | "open_time": "2020.06.25 04:50:17", 681 | "order_type": 0, 682 | "profit": -7.0, 683 | "sl": 0.0, 684 | "swap": 0.0, 685 | "symbol": "EURUSD", 686 | "ticket": 124114600, 687 | "tp": 0.0 688 | } 689 | } 690 | ``` 691 | -------------------------------------------------------------------------------- /docs/dev.md: -------------------------------------------------------------------------------- 1 | # Development Environment 2 | The main source code file is [ZeroMQ_Server.mq4](../metatrader4/MQL4/Scripts/ZeroMQ_Server.mq4). 3 | It is recommended to use a JetBrains IDE with the [MQL Idea](https://plugins.jetbrains.com/plugin/9291-mql-idea) plugin for syntax highlighting and code completion. 4 | The source code, dependencies, and configuration files are deployed to a MetaTrader 4 server via SSH. 5 | The deployment script is written in Bash, so that either Linux or Windows (via WSL) can be used for development. 6 | 7 | ## MetaTrader 4 Server Setup 8 | - obtain a demo account from a ForEx broker and download their MetaTrader 4 installer 9 | - install MetaTrader 4 on a server (e.g. an evaluation install of Windows 10 on a virtual machine) 10 | - install WSL (Windows Subsystem for Linux) on the server 11 | - run a SSH server within WSL 12 | - configure password-less login using an SSH key 13 | - enable SSH master sessions on your dev workstation 14 | 15 | ## Dev Workstation Setup 16 | - create a copy of [zeromq_server_startup.template.ini](../metatrader4/config/zeromq_server_startup.template.ini), in that same folder, and rename it to `zeromq_server_startup.ini` 17 | - edit `zeromq_server_startup.ini` and set `Login`, `Password`, and `Server` values pertaining to your demo account 18 | - edit [remote_deploy.sh](../remote_deploy.sh) and configure the variables near the top of the script 19 | - edit [restart_mt4.bat](../metatrader4/restart_mt4.bat) and configure the variables near the top of the script 20 | 21 | ## Deployment 22 | To deploy the latest code, run [remote_deploy.sh](../remote_deploy.sh) on the dev workstation. 23 | This will copy the server script and its dependencies into the remote MT4 profile directory and compile them. 24 | To apply the changes, restart MetaTrader 4 by running [restart_mt4.bat](../metatrader4/restart_mt4.bat) in the remote MT4 profile directory. 25 | -------------------------------------------------------------------------------- /metatrader4/MQL4/Include/json/JAson.mqh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoeJoder/metatrader4-server/fc3ab36d6824eade1826c2a81ae06e78cf92006f/metatrader4/MQL4/Include/json/JAson.mqh -------------------------------------------------------------------------------- /metatrader4/MQL4/Include/json/README.md: -------------------------------------------------------------------------------- 1 | # JAson.mqh 2 | JSON serialization and deserialization (native MQL) for MetaTrader 4/5. 3 | 4 | ## Description 5 | This code is ported from a high-speed С++ library. It was [originally posted](https://www.mql5.com/en/code/13663) on the MetaTrader code forums. 6 | 7 | ## Examples 8 | ```mql4 9 | string in, out; 10 | CJAVal js(NULL, jtUNDEF); bool b; 11 | 12 | //--- 13 | Print("JASon Example Deserialization:"); 14 | 15 | in="{\"a\":[1,2]}"; out=""; // example of input data 16 | b=js.Deserialize(in); // deserialized 17 | js.Serialize(out); // serialized again 18 | Print(in+" -> "+out); // output for comparison 19 | 20 | //--- 21 | Print("JASon Example Serialization:"); 22 | 23 | js["Test"]=1.4; // input data example 24 | out=""; js.Serialize(out); // serialized 25 | Print(out); // output 26 | ``` 27 | 28 | Authorization on a website and parsing the response: 29 | ```mql4 30 | CJAVal jv; 31 | jv["login"]="Login"; // login 32 | jv["password"]="Pass"; // password 33 | 34 | //--- serialize to string {"login":"Login","password":"Pass"} 35 | char data[]; 36 | ArrayResize(data, StringToCharArray(jv.Serialize(), data, 0, WHOLE_ARRAY)-1); 37 | 38 | //--- send data 39 | char res_data[]; 40 | string res_headers=NULL; 41 | int r=WebRequest("POST", "http://my.site.com/Authorize", "Content-Type: text/plain\r\n", 5000, data, res_data, res_headers); 42 | 43 | //--- assume the answer {"accessToken":"ABRAKADABRA","session_id":124521} 44 | //--- get AccessToken 45 | jv.Deserialize(res_data); 46 | string AccessToken=jv["accessToken"].ToStr(); 47 | ``` 48 | 49 | ## Author 50 | [sergeev](https://www.mql5.com/en/users/sergeev) 51 | -------------------------------------------------------------------------------- /metatrader4/MQL4/Scripts/ZeroMQ_Server.mq4: -------------------------------------------------------------------------------- 1 | #property description "An endpoint for remote control of MetaTrader 4 via ZeroMQ sockets." 2 | #property copyright "Copyright 2020, CoeJoder" 3 | #property link "https://github.com/CoeJoder/metatrader4-server" 4 | #property version "1.0" 5 | #property strict 6 | #property show_inputs 7 | 8 | #include 9 | // see: https://github.com/dingmaotu/mql-zmq 10 | #include 11 | // see: https://www.mql5.com/en/code/13663 12 | #include 13 | 14 | // input parameters 15 | extern string SCRIPT_NAME = "ZeroMQ_Server"; 16 | extern string ADDRESS = "tcp://*:28282"; 17 | extern int REQUEST_POLLING_INTERVAL = 500; 18 | extern int RESPONSE_TIMEOUT = 5000; 19 | extern int MIN_POINT_DISTANCE = 3; 20 | extern bool VERBOSE = true; 21 | 22 | // response message keys 23 | const string KEY_RESPONSE = "response"; 24 | const string KEY_ERROR_CODE = "error_code"; 25 | const string KEY_ERROR_CODE_DESCRIPTION = "error_code_description"; 26 | const string KEY_ERROR_MESSAGE = "error_message"; 27 | const string KEY_WARNING = "warning"; 28 | 29 | // types of requests 30 | enum RequestAction { 31 | GET_ACCOUNT_INFO, 32 | GET_ACCOUNT_INFO_INTEGER, 33 | GET_ACCOUNT_INFO_DOUBLE, 34 | GET_SYMBOL_INFO, 35 | GET_SYMBOL_MARKET_INFO, 36 | GET_SYMBOL_INFO_INTEGER, 37 | GET_SYMBOL_INFO_DOUBLE, 38 | GET_SYMBOL_INFO_STRING, 39 | GET_SYMBOL_TICK, 40 | GET_ORDER, 41 | GET_ORDERS, 42 | GET_HISTORICAL_ORDERS, 43 | GET_SYMBOLS, 44 | GET_OHLCV, 45 | GET_SIGNALS, 46 | GET_SIGNAL_INFO, 47 | DO_ORDER_SEND, 48 | DO_ORDER_CLOSE, 49 | DO_ORDER_DELETE, 50 | DO_ORDER_MODIFY, 51 | RUN_INDICATOR 52 | }; 53 | 54 | // types of indicators 55 | enum Indicator { 56 | iAC, 57 | iAD, 58 | iADX, 59 | iAlligator, 60 | iAO, 61 | iATR, 62 | iBearsPower, 63 | iBands, 64 | iBandsOnArray, 65 | iBullsPower, 66 | iCCI, 67 | iCCIOnArray, 68 | iCustom, 69 | iDeMarker, 70 | iEnvelopes, 71 | iEnvelopesOnArray, 72 | iForce, 73 | iFractals, 74 | iGator, 75 | iIchimoku, 76 | iBWMFI, 77 | iMomentum, 78 | iMomentumOnArray, 79 | iMFI, 80 | iMA, 81 | iMAOnArray, 82 | iOsMA, 83 | iMACD, 84 | iOBV, 85 | iSAR, 86 | iRSI, 87 | iRSIOnArray, 88 | iRVI, 89 | iStdDev, 90 | iStdDevOnArray, 91 | iStochastic, 92 | iWPR 93 | }; 94 | 95 | // ZeroMQ sockets 96 | Context* context = NULL; 97 | Socket* socket = NULL; 98 | 99 | int OnInit() { 100 | ENUM_INIT_RETCODE retcode = INIT_SUCCEEDED; 101 | 102 | // workaround for OnInit() being called twice when script is attached via .ini at terminal startup 103 | if (context == NULL) { 104 | // ZeroMQ context and sockets 105 | context = new Context(SCRIPT_NAME); 106 | context.setBlocky(false); 107 | socket = new Socket(context, ZMQ_REP); 108 | socket.setSendHighWaterMark(1); 109 | socket.setReceiveHighWaterMark(1); 110 | socket.setSendTimeout(RESPONSE_TIMEOUT); 111 | if (!socket.bind(ADDRESS)) { 112 | Alert(StringFormat("Failed to bind socket on %s: %s", ADDRESS, Zmq::errorMessage(Zmq::errorNumber()))); 113 | retcode = INIT_FAILED; 114 | } 115 | else { 116 | Print(StringFormat("Listening for requests on %s", ADDRESS)); 117 | } 118 | } 119 | return retcode; 120 | } 121 | 122 | void OnDeinit(const int reason) { 123 | if (context != NULL) { 124 | Print("Unbinding listening socket..."); 125 | socket.unbind(ADDRESS); 126 | 127 | // destroy ZeroMQ context 128 | context.destroy(0); 129 | 130 | // deallocate ZeroMQ objects 131 | delete socket; 132 | delete context; 133 | socket = NULL; 134 | context = NULL; 135 | } 136 | } 137 | 138 | void OnStart() { 139 | PollItem poller[1]; 140 | socket.fillPollItem(poller[0], ZMQ_POLLIN); 141 | ZmqMsg inMessage; 142 | while (IsRunning()) { 143 | if (-1 == Socket::poll(poller, REQUEST_POLLING_INTERVAL)) { 144 | Print("Failed input polling: " + Zmq::errorMessage(Zmq::errorNumber())); 145 | } 146 | else if (poller[0].hasInput()) { 147 | if (_socketReceive(inMessage, true)) { 148 | if (inMessage.size() > 0) { 149 | string dataStr = inMessage.getData(); 150 | Trace("Received request: " + dataStr); 151 | // responsible for sending response 152 | _processRequest(dataStr); 153 | } 154 | else { 155 | sendError("Request was empty."); 156 | } 157 | } 158 | } 159 | } 160 | } 161 | 162 | bool _socketReceive(ZmqMsg& msg, bool nowait=false) { 163 | bool success = true; 164 | if (!socket.recv(msg, nowait)) { 165 | Print("Failed to receive request."); 166 | success = false; 167 | } 168 | return success; 169 | } 170 | 171 | bool _socketSend(string response=NULL, bool nowait=false) { 172 | bool success = true; 173 | if ((response == NULL && !socket.send(nowait)) || (response != NULL && !socket.send(response, nowait))) { 174 | Alert("Critical error! Failed to send response to client: " + Zmq::errorMessage(Zmq::errorNumber())); 175 | success = false; 176 | } 177 | return success; 178 | } 179 | 180 | void _processRequest(string dataStr) { 181 | // parse JSON request 182 | CJAVal req; 183 | if (!req.Deserialize(dataStr, CP_UTF8)) { 184 | sendError("Failed to parse request."); 185 | return; 186 | } 187 | string actionStr = req["action"].ToStr(); 188 | if (actionStr == "") { 189 | sendError("No request action specified."); 190 | return; 191 | } 192 | 193 | // perform the action 194 | RequestAction action = (RequestAction)-1; 195 | switch(StringToEnum(actionStr, action)) { 196 | case GET_ACCOUNT_INFO: 197 | Get_AccountInfo(); 198 | break; 199 | case GET_ACCOUNT_INFO_INTEGER: 200 | Get_AccountInfoInteger(req); 201 | break; 202 | case GET_ACCOUNT_INFO_DOUBLE: 203 | Get_AccountInfoDouble(req); 204 | break; 205 | case GET_SYMBOL_INFO: 206 | Get_SymbolInfo(req); 207 | break; 208 | case GET_SYMBOL_MARKET_INFO: 209 | Get_SymbolMarketInfo(req); 210 | break; 211 | case GET_SYMBOL_INFO_INTEGER: 212 | Get_SymbolInfoInteger(req); 213 | break; 214 | case GET_SYMBOL_INFO_DOUBLE: 215 | Get_SymbolInfoDouble(req); 216 | break; 217 | case GET_SYMBOL_INFO_STRING: 218 | Get_SymbolInfoString(req); 219 | break; 220 | case GET_SYMBOL_TICK: 221 | Get_SymbolTick(req); 222 | break; 223 | case GET_ORDER: 224 | Get_Order(req); 225 | break; 226 | case GET_ORDERS: 227 | Get_Orders(); 228 | break; 229 | case GET_HISTORICAL_ORDERS: 230 | Get_HistoricalOrders(); 231 | break; 232 | case GET_SYMBOLS: 233 | Get_Symbols(); 234 | break; 235 | case GET_OHLCV: 236 | Get_OHLCV(req); 237 | break; 238 | case GET_SIGNALS: 239 | Get_Signals(); 240 | break; 241 | case GET_SIGNAL_INFO: 242 | Get_SignalInfo(req); 243 | break; 244 | case DO_ORDER_SEND: 245 | Do_OrderSend(req); 246 | break; 247 | case DO_ORDER_MODIFY: 248 | Do_OrderModify(req); 249 | break; 250 | case DO_ORDER_CLOSE: 251 | Do_OrderClose(req); 252 | break; 253 | case DO_ORDER_DELETE: 254 | Do_OrderDelete(req); 255 | break; 256 | case RUN_INDICATOR: 257 | Run_Indicator(req); 258 | break; 259 | default: { 260 | string errorStr = StringFormat("Unrecognized requested action (%s).", actionStr); 261 | Print(errorStr); 262 | sendError(errorStr); 263 | break; 264 | } 265 | } 266 | } 267 | 268 | void _serializeAndSendResponse(CJAVal& resp) { 269 | string strResp = resp.Serialize(); 270 | if (_socketSend(strResp)) { 271 | Trace("Sent response: " + strResp); 272 | } 273 | } 274 | 275 | void sendResponse(CJAVal& data, string warning=NULL) { 276 | CJAVal resp; 277 | resp[KEY_RESPONSE].Set(data); 278 | if (warning != NULL) { 279 | resp[KEY_WARNING] = warning; 280 | } 281 | _serializeAndSendResponse(resp); 282 | } 283 | 284 | void sendResponse(string val, string warning=NULL) { 285 | CJAVal resp; 286 | resp[KEY_RESPONSE] = val; 287 | if (warning != NULL) { 288 | resp[KEY_WARNING] = warning; 289 | } 290 | _serializeAndSendResponse(resp); 291 | } 292 | 293 | void sendResponse(double val, string warning=NULL) { 294 | CJAVal resp; 295 | resp[KEY_RESPONSE] = val; 296 | if (warning != NULL) { 297 | resp[KEY_WARNING] = warning; 298 | } 299 | _serializeAndSendResponse(resp); 300 | } 301 | 302 | void sendResponse(long val, string warning=NULL) { 303 | CJAVal resp; 304 | resp[KEY_RESPONSE] = val; 305 | if (warning != NULL) { 306 | resp[KEY_WARNING] = warning; 307 | } 308 | _serializeAndSendResponse(resp); 309 | } 310 | 311 | void sendError(int code, string msg) { 312 | CJAVal resp; 313 | resp[KEY_ERROR_CODE] = code; 314 | resp[KEY_ERROR_CODE_DESCRIPTION] = ErrorDescription(code); 315 | resp[KEY_ERROR_MESSAGE] = msg; 316 | _serializeAndSendResponse(resp); 317 | } 318 | 319 | void sendError(int code) { 320 | CJAVal resp; 321 | resp[KEY_ERROR_CODE] = code; 322 | resp[KEY_ERROR_CODE_DESCRIPTION] = ErrorDescription(code); 323 | _serializeAndSendResponse(resp); 324 | } 325 | 326 | void sendError(string msg) { 327 | CJAVal resp; 328 | resp[KEY_ERROR_MESSAGE] = msg; 329 | _serializeAndSendResponse(resp); 330 | } 331 | 332 | void sendErrorMissingParam(string paramName) { 333 | sendError(StringFormat("Missing \"%s\" param.", paramName)); 334 | } 335 | 336 | bool assertParamExists(CJAVal& req, string paramName) { 337 | if (IsNullOrMissing(req, paramName)) { 338 | sendErrorMissingParam(paramName); 339 | return false; 340 | } 341 | return true; 342 | } 343 | 344 | bool assertParamArrayExistsAndNotEmpty(CJAVal& req, string paramName) { 345 | if (!assertParamExists(req, paramName)) { 346 | return false; 347 | } 348 | CJAVal* param = req[paramName]; 349 | if (param.m_type != jtARRAY) { 350 | sendError(StringFormat("Param \"%s\" is not an array.", paramName)); 351 | return false; 352 | } 353 | if (param.Size() == 0) { 354 | sendError(StringFormat("Param \"%s[]\" is empty.", paramName)); 355 | return false; 356 | } 357 | return true; 358 | } 359 | 360 | // selects an order and sends it to the client, or sends an error code if not found 361 | void sendOrder(int ticket, string warning=NULL) { 362 | if (OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES)) { 363 | // order is pending or open 364 | CJAVal newOrder; 365 | _getSelectedOrder(newOrder); 366 | sendResponse(newOrder, warning); 367 | return; 368 | } 369 | else { 370 | // order was not found in Trades tab; check Account History tab 371 | if (OrderSelect(ticket, SELECT_BY_TICKET, MODE_HISTORY)) { 372 | // order is closed 373 | CJAVal newOrder; 374 | _getSelectedOrder(newOrder); 375 | sendResponse(newOrder, warning); 376 | return; 377 | } 378 | else { 379 | // order was not found; this should never happen for a valid ticket # unless there is a server error 380 | sendError(StringFormat("Order # %d is not found in the Trades or Account History tabs.", ticket)); 381 | return; 382 | } 383 | } 384 | } 385 | 386 | void Get_AccountInfo() { 387 | long login = AccountInfoInteger(ACCOUNT_LOGIN); 388 | long tradeMode = AccountInfoInteger(ACCOUNT_TRADE_MODE); 389 | string name = AccountInfoString(ACCOUNT_NAME); 390 | string server = AccountInfoString(ACCOUNT_SERVER); 391 | string currency = AccountInfoString(ACCOUNT_CURRENCY); 392 | string company = AccountInfoString(ACCOUNT_COMPANY); 393 | 394 | CJAVal account_info; 395 | account_info["login"] = login; 396 | account_info["trade_mode"] = tradeMode; 397 | account_info["name"] = name; 398 | account_info["server"] = server; 399 | account_info["currency"] = currency; 400 | account_info["company"] = company; 401 | sendResponse(account_info); 402 | } 403 | 404 | void Get_AccountInfoInteger(CJAVal& req) { 405 | // use either property's name or id, giving priority to name 406 | if (!IsNullOrMissing(req, "property_name")) { 407 | string propertyName = req["property_name"].ToStr(); 408 | ENUM_ACCOUNT_INFO_INTEGER action = (ENUM_ACCOUNT_INFO_INTEGER)-1; 409 | action = StringToEnum(propertyName, action); 410 | if (action == -1) { 411 | sendError(StringFormat("Unrecognized account integer property: %s", propertyName)); 412 | return; 413 | } 414 | else { 415 | sendResponse(AccountInfoInteger(action)); 416 | return; 417 | } 418 | } 419 | else if (!IsNullOrMissing(req, "property_id")) { 420 | int propertyId = (int)req["property_id"].ToInt(); 421 | sendResponse(AccountInfoInteger(propertyId)); 422 | return; 423 | } 424 | else { 425 | sendError("Must include either \"property_name\" or \"property_id\" param."); 426 | return; 427 | } 428 | } 429 | 430 | void Get_AccountInfoDouble(CJAVal& req) { 431 | // use either property's name or id, giving priority to name 432 | if (!IsNullOrMissing(req, "property_name")) { 433 | string propertyName = req["property_name"].ToStr(); 434 | ENUM_ACCOUNT_INFO_DOUBLE action = (ENUM_ACCOUNT_INFO_DOUBLE)-1; 435 | action = StringToEnum(propertyName, action); 436 | if (action == -1) { 437 | sendError(StringFormat("Unrecognized account double property: %s", propertyName)); 438 | return; 439 | } 440 | else { 441 | sendResponse(AccountInfoDouble(action)); 442 | return; 443 | } 444 | } 445 | else if (!IsNullOrMissing(req, "property_id")) { 446 | int propertyId = (int)req["property_id"].ToInt(); 447 | sendResponse(AccountInfoDouble(propertyId)); 448 | return; 449 | } 450 | else { 451 | sendError("Must include either \"property_name\" or \"property_id\" param."); 452 | return; 453 | } 454 | } 455 | 456 | void Get_SymbolInfo(CJAVal& req) { 457 | if (!assertParamArrayExistsAndNotEmpty(req, "names")) { 458 | return; 459 | } 460 | CJAVal* names = req["names"]; 461 | CJAVal symbols; 462 | for (int i = 0; i < names.Size(); i++) { 463 | string name = names[i].ToStr(); 464 | if (!SymbolSelect(name, true)) { 465 | sendError(GetLastError(), name); 466 | return; 467 | } 468 | double point = SymbolInfoDouble(name, SYMBOL_POINT); // Point size in the quote currency 469 | long digits = SymbolInfoInteger(name, SYMBOL_DIGITS); // Digits after decimal point 470 | double volume_min = SymbolInfoDouble(name, SYMBOL_VOLUME_MIN); // Minimal volume for a deal 471 | double volume_step = SymbolInfoDouble(name, SYMBOL_VOLUME_STEP); // Minimal volume change step for deal execution 472 | double volume_max = SymbolInfoDouble(name, SYMBOL_VOLUME_MAX); // Maximal volume for a deal 473 | double trade_contract_size = SymbolInfoDouble(name, SYMBOL_TRADE_CONTRACT_SIZE); // Trade contract size in the base currency 474 | double trade_tick_value = SymbolInfoDouble(name, SYMBOL_TRADE_TICK_VALUE); // Tick value in the deposit currency 475 | double trade_tick_size = SymbolInfoDouble(name, SYMBOL_TRADE_TICK_SIZE); // Tick size in points 476 | long trade_stops_level = SymbolInfoInteger(name, SYMBOL_TRADE_STOPS_LEVEL); // Stop level in points 477 | long trade_freeze_level = SymbolInfoInteger(name, SYMBOL_TRADE_FREEZE_LEVEL); // Order freeze level in points 478 | 479 | CJAVal symbol; 480 | symbol["name"] = name; 481 | symbol["point"] = point; 482 | symbol["digits"] = digits; 483 | symbol["volume_min"] = volume_min; 484 | symbol["volume_step"] = volume_step; 485 | symbol["volume_max"] = volume_max; 486 | symbol["trade_contract_size"] = trade_contract_size; 487 | symbol["trade_tick_value"] = trade_tick_value; 488 | symbol["trade_tick_size"] = trade_tick_size; 489 | symbol["trade_stops_level"] = trade_stops_level; 490 | symbol["trade_freeze_level"] = trade_freeze_level; 491 | symbols[name].Set(symbol); 492 | } 493 | sendResponse(symbols); 494 | return; 495 | } 496 | 497 | void Get_SymbolMarketInfo(CJAVal& req) { 498 | if (!assertParamExists(req, "symbol") || !assertParamExists(req, "property")) { 499 | return; 500 | } 501 | string symbol = req["symbol"].ToStr(); 502 | string strProperty = req["property"].ToStr(); 503 | 504 | if (!SymbolSelect(symbol, true)) { 505 | sendError(GetLastError(), symbol); 506 | return; 507 | } 508 | 509 | ENUM_MARKETINFO prop = (ENUM_MARKETINFO)-1; 510 | prop = StringToEnum(strProperty, prop); 511 | if (prop != -1) { 512 | sendResponse(MarketInfo(symbol, prop)); 513 | return; 514 | } 515 | else { 516 | sendError(StringFormat("Unrecognized market info property: %s", strProperty)); 517 | return; 518 | } 519 | } 520 | 521 | void Get_SymbolInfoInteger(CJAVal& req) { 522 | if (!assertParamExists(req, "symbol")) { 523 | return; 524 | } 525 | string symbol = req["symbol"].ToStr(); 526 | 527 | // use either property's name or id, giving priority to name 528 | if (!IsNullOrMissing(req, "property_name")) { 529 | string propertyName = req["property_name"].ToStr(); 530 | ENUM_SYMBOL_INFO_INTEGER action = (ENUM_SYMBOL_INFO_INTEGER)-1; 531 | action = StringToEnum(propertyName, action); 532 | if (action == -1) { 533 | sendError(StringFormat("Unrecognized symbol integer property: %s", propertyName)); 534 | return; 535 | } 536 | else { 537 | long propertyValue; 538 | if (!SymbolInfoInteger(symbol, action, propertyValue)) { 539 | sendError(GetLastError()); 540 | return; 541 | } 542 | else { 543 | sendResponse(propertyValue); 544 | return; 545 | } 546 | } 547 | } 548 | else if (!IsNullOrMissing(req, "property_id")) { 549 | int propertyId = (int)req["property_id"].ToInt(); 550 | long propertyValue; 551 | if (!SymbolInfoInteger(symbol, propertyId, propertyValue)) { 552 | sendError(GetLastError()); 553 | return; 554 | } 555 | else { 556 | sendResponse(propertyValue); 557 | return; 558 | } 559 | } 560 | else { 561 | sendError("Must include either \"property_name\" or \"property_id\" param."); 562 | return; 563 | } 564 | } 565 | 566 | void Get_SymbolInfoDouble(CJAVal& req) { 567 | if (!assertParamExists(req, "symbol")) { 568 | return; 569 | } 570 | string symbol = req["symbol"].ToStr(); 571 | 572 | // use either property's name or id, giving priority to name 573 | if (!IsNullOrMissing(req, "property_name")) { 574 | string propertyName = req["property_name"].ToStr(); 575 | ENUM_SYMBOL_INFO_DOUBLE action = (ENUM_SYMBOL_INFO_DOUBLE)-1; 576 | action = StringToEnum(propertyName, action); 577 | if (action == -1) { 578 | sendError(StringFormat("Unrecognized symbol double property: %s", propertyName)); 579 | return; 580 | } 581 | else { 582 | double propertyValue; 583 | if (!SymbolInfoDouble(symbol, action, propertyValue)) { 584 | sendError(GetLastError()); 585 | return; 586 | } 587 | else { 588 | sendResponse(propertyValue); 589 | return; 590 | } 591 | } 592 | } 593 | else if (!IsNullOrMissing(req, "property_id")) { 594 | int propertyId = (int)req["property_id"].ToInt(); 595 | double propertyValue; 596 | if (!SymbolInfoDouble(symbol, propertyId, propertyValue)) { 597 | sendError(GetLastError()); 598 | return; 599 | } 600 | else { 601 | sendResponse(propertyValue); 602 | return; 603 | } 604 | } 605 | else { 606 | sendError("Must include either \"property_name\" or \"property_id\" param."); 607 | return; 608 | } 609 | } 610 | 611 | void Get_SymbolInfoString(CJAVal& req) { 612 | if (!assertParamExists(req, "symbol")) { 613 | return; 614 | } 615 | string symbol = req["symbol"].ToStr(); 616 | 617 | // use either property's name or id, giving priority to name 618 | if (!IsNullOrMissing(req, "property_name")) { 619 | string propertyName = req["property_name"].ToStr(); 620 | ENUM_SYMBOL_INFO_STRING action = (ENUM_SYMBOL_INFO_STRING)-1; 621 | action = StringToEnum(propertyName, action); 622 | if (action == -1) { 623 | sendError(StringFormat("Unrecognized symbol string property: %s", propertyName)); 624 | return; 625 | } 626 | else { 627 | string propertyValue; 628 | if (!SymbolInfoString(symbol, action, propertyValue)) { 629 | sendError(GetLastError()); 630 | return; 631 | } 632 | else { 633 | sendResponse(propertyValue); 634 | return; 635 | } 636 | } 637 | } 638 | else if (!IsNullOrMissing(req, "property_id")) { 639 | int propertyId = (int)req["property_id"].ToInt(); 640 | string propertyValue; 641 | if (!SymbolInfoString(symbol, propertyId, propertyValue)) { 642 | sendError(GetLastError()); 643 | return; 644 | } 645 | else { 646 | sendResponse(propertyValue); 647 | return; 648 | } 649 | } 650 | else { 651 | sendError("Must include either \"property_name\" or \"property_id\" param."); 652 | return; 653 | } 654 | } 655 | 656 | void Get_SymbolTick(CJAVal& req) { 657 | if (!assertParamExists(req, "symbol")) { 658 | return; 659 | } 660 | string symbol = req["symbol"].ToStr(); 661 | 662 | if (!SymbolSelect(symbol, true)) { 663 | sendError(GetLastError(), symbol); 664 | return; 665 | } 666 | 667 | MqlTick lastTick; 668 | if(SymbolInfoTick(symbol, lastTick)) { 669 | CJAVal tick; 670 | tick["time"] = (long)lastTick.time; 671 | tick["bid"] = lastTick.bid; 672 | tick["ask"] = lastTick.ask; 673 | tick["last"] = lastTick.last; 674 | tick["volume"] = (long)lastTick.volume; 675 | sendResponse(tick); 676 | return; 677 | } 678 | else { 679 | sendError(GetLastError()); 680 | return; 681 | } 682 | } 683 | 684 | void Get_Order(CJAVal& req) { 685 | if (!assertParamExists(req, "ticket")) { 686 | return; 687 | } 688 | int ticket = (int)req["ticket"].ToInt(); 689 | sendOrder(ticket); 690 | } 691 | 692 | void Get_Orders() { 693 | _getOrders(MODE_TRADES); 694 | } 695 | 696 | void Get_HistoricalOrders() { 697 | _getOrders(MODE_HISTORY); 698 | } 699 | 700 | void _getOrders(int mode) { 701 | CJAVal orders; 702 | int total = (mode == MODE_HISTORY ? OrdersHistoryTotal() : OrdersTotal()); 703 | // always count backwards 704 | for(int pos = total - 1; pos >= 0; pos--) { 705 | if (!OrderSelect(pos, SELECT_BY_POS, mode)) { 706 | int errorCode = GetLastError(); 707 | sendError(errorCode, StringFormat("Failed to select order # %d.", pos)); 708 | return; 709 | } 710 | else { 711 | // order selected 712 | CJAVal curOrder; 713 | _getSelectedOrder(curOrder); 714 | orders.Add(curOrder); 715 | } 716 | } 717 | sendResponse(orders); 718 | } 719 | 720 | void _getSelectedOrder(CJAVal& order) { 721 | order["ticket"] = OrderTicket(); 722 | order["magic_number"] = OrderMagicNumber(); 723 | order["symbol"] = OrderSymbol(); 724 | order["order_type"] = OrderType(); 725 | order["lots"] = OrderLots(); 726 | order["open_price"] = OrderOpenPrice(); 727 | order["close_price"] = OrderClosePrice(); 728 | order["open_time"] = TimeToStr(OrderOpenTime(), TIME_DATE|TIME_SECONDS); 729 | order["close_time"] = TimeToStr(OrderCloseTime(), TIME_DATE|TIME_SECONDS); 730 | order["expiration"] = TimeToStr(OrderExpiration(), TIME_DATE|TIME_SECONDS); 731 | order["sl"] = OrderStopLoss(); 732 | order["tp"] = OrderTakeProfit(); 733 | order["profit"] = OrderProfit(); 734 | order["commission"] = OrderCommission(); 735 | order["swap"] = OrderSwap(); 736 | order["comment"] = OrderComment(); 737 | } 738 | 739 | void Get_Symbols() { 740 | CJAVal symbols; 741 | bool onlyMarketWatch = false; 742 | int count = SymbolsTotal(onlyMarketWatch); 743 | for (int i = 0; i < count; i++) { 744 | symbols.Add(SymbolName(i, onlyMarketWatch)); 745 | } 746 | sendResponse(symbols); 747 | } 748 | 749 | void Get_OHLCV(CJAVal& req) { 750 | if (!assertParamExists(req, "symbol") || !assertParamExists(req, "timeframe") 751 | || !assertParamExists(req, "limit") || !assertParamExists(req, "timeout")) { 752 | return; 753 | } 754 | string symbol = req["symbol"].ToStr(); 755 | int timeframe = (int)req["timeframe"].ToInt(); 756 | int limit = (int)req["limit"].ToInt(); 757 | long timeout = req["timeout"].ToInt(); 758 | datetime now = TimeCurrent(); 759 | 760 | if (!SymbolSelect(symbol, true)) { 761 | sendError(GetLastError(), symbol); 762 | return; 763 | } 764 | 765 | MqlRates rates[]; 766 | bool reverseOrder = false; // oldest-to-newest 767 | ArraySetAsSeries(rates, reverseOrder); 768 | 769 | // need to poll, as data may not be immediately available 770 | int delay = 100; // milliseconds 771 | long maxTries = timeout / delay; 772 | int numResults = -1; 773 | for (int try = 0; try < maxTries && numResults == -1; try++) { 774 | numResults = CopyRates(symbol, timeframe, now, limit, rates); 775 | if (numResults == -1) { 776 | Sleep(delay); 777 | } 778 | } 779 | if (numResults == -1) { 780 | sendError("Timed out waiting for OHLCV data."); 781 | return; 782 | } 783 | else { 784 | // success 785 | CJAVal ohlcv; 786 | for (int i = 0; i < numResults; i++) { 787 | CJAVal curBar; 788 | curBar["time"] = (long)rates[i].time; 789 | curBar["open"] = rates[i].open; 790 | curBar["high"] = rates[i].high; 791 | curBar["low"] = rates[i].low; 792 | curBar["close"] = rates[i].close; 793 | curBar["tick_volume"] = rates[i].tick_volume; 794 | ohlcv.Add(curBar); 795 | } 796 | sendResponse(ohlcv); 797 | return; 798 | } 799 | } 800 | 801 | void Get_Signals() { 802 | CJAVal signals; 803 | int total = SignalBaseTotal(); 804 | for (int i = 0; i < total; i++) { 805 | if (!SignalBaseSelect(i)) { 806 | sendError(GetLastError()); 807 | return; 808 | } 809 | else { 810 | signals.Add(SignalBaseGetString(SIGNAL_BASE_NAME)); 811 | } 812 | } 813 | sendResponse(signals); 814 | } 815 | 816 | void Get_SignalInfo(CJAVal& req) { 817 | if (!assertParamArrayExistsAndNotEmpty(req, "names")) { 818 | return; 819 | } 820 | CJAVal* reqNames = req["names"]; 821 | CJAVal signals; 822 | int total = SignalBaseTotal(); 823 | for (int i = 0; i < total; i++) { 824 | if (!SignalBaseSelect(i)) { 825 | sendError(GetLastError()); 826 | return; 827 | } 828 | else { 829 | // signal selected 830 | string name = SignalBaseGetString(SIGNAL_BASE_NAME); 831 | if (ArrayEraseElement(reqNames.m_e, name)) { 832 | CJAVal signal; 833 | signal["author_login"] = SignalBaseGetString(SIGNAL_BASE_AUTHOR_LOGIN); 834 | signal["broker"] = SignalBaseGetString(SIGNAL_BASE_BROKER); 835 | signal["broker_server"] = SignalBaseGetString(SIGNAL_BASE_BROKER_SERVER); 836 | signal["name"] = name; 837 | signal["currency"] = SignalBaseGetString(SIGNAL_BASE_CURRENCY); 838 | signal["date_published"] = SignalBaseGetInteger(SIGNAL_BASE_DATE_PUBLISHED); 839 | signal["date_started"] = SignalBaseGetInteger(SIGNAL_BASE_DATE_STARTED); 840 | signal["id"] = SignalBaseGetInteger(SIGNAL_BASE_ID); 841 | signal["leverage"] = SignalBaseGetInteger(SIGNAL_BASE_LEVERAGE); 842 | signal["pips"] = SignalBaseGetInteger(SIGNAL_BASE_PIPS); 843 | signal["rating"] = SignalBaseGetInteger(SIGNAL_BASE_RATING); 844 | signal["subscribers"] = SignalBaseGetInteger(SIGNAL_BASE_SUBSCRIBERS); 845 | signal["trades"] = SignalBaseGetInteger(SIGNAL_BASE_TRADES); 846 | signal["trade_mode"] = SignalBaseGetInteger(SIGNAL_BASE_TRADE_MODE); 847 | signal["balance"] = SignalBaseGetDouble(SIGNAL_BASE_BALANCE); 848 | signal["equity"] = SignalBaseGetDouble(SIGNAL_BASE_EQUITY); 849 | signal["gain"] = SignalBaseGetDouble(SIGNAL_BASE_GAIN); 850 | signal["max_drawdown"] = SignalBaseGetDouble(SIGNAL_BASE_MAX_DRAWDOWN); 851 | signal["price"] = SignalBaseGetDouble(SIGNAL_BASE_PRICE); 852 | signal["roi"] = SignalBaseGetDouble(SIGNAL_BASE_ROI); 853 | signals[name].Set(signal); 854 | } 855 | } 856 | } 857 | if (reqNames.Size() == 0) { 858 | sendResponse(signals); 859 | return; 860 | } 861 | else { 862 | sendError(StringFormat("Signals not found: %s", reqNames.Serialize())); 863 | return; 864 | } 865 | } 866 | 867 | void Do_OrderSend(CJAVal& req) { 868 | if (!assertParamExists(req, "symbol") || !assertParamExists(req, "order_type") 869 | || !assertParamExists(req, "lots") || !assertParamExists(req, "comment")) { 870 | return; 871 | } 872 | 873 | // stop-loss and take-profit params must be either relative or absolute, but not both 874 | if (!IsNullOrMissing(req, "sl") && !IsNullOrMissing(req, "sl_points")) { 875 | sendError("Stop-loss cannot be both relative (sl_points) and absolute (sl). Specify one or the other."); 876 | return; 877 | } 878 | if (!IsNullOrMissing(req, "tp") && !IsNullOrMissing(req, "tp_points")) { 879 | sendError("Take-profit cannot be both relative (tp_points) and absolute (tp). Specify one or the other."); 880 | return; 881 | } 882 | 883 | string symbol = req["symbol"].ToStr(); 884 | int orderType = (int)req["order_type"].ToInt(); 885 | double lots = req["lots"].ToDbl(); 886 | string comment = req["comment"].ToStr(); 887 | 888 | if (!SymbolSelect(symbol, true)) { 889 | sendError(GetLastError(), symbol); 890 | return; 891 | } 892 | 893 | if (!IsValidTradeOperation(orderType)) { 894 | sendError(StringFormat("Invalid trade operation: %d", orderType)); 895 | return; 896 | } 897 | 898 | if (IsPendingOrder(orderType) && IsNullOrMissing(req, "price")) { 899 | sendError("Cannot place a pending order without the \"price\" parameter."); 900 | return; 901 | } 902 | 903 | // use default values as needed for the optional request params 904 | double price = GetDefault(req, "price", DefaultOpenPrice(symbol, orderType)); 905 | int slippage = GetDefault(req, "slippage", DefaultSlippage(symbol)); 906 | double stopLoss = GetDefault(req, "sl", (double)NULL); 907 | double takeProfit = GetDefault(req, "tp", (double)NULL); 908 | int stopLossPoints = GetDefault(req, "sl_points", (int)NULL); 909 | int takeProfitPoints = GetDefault(req, "tp_points", (int)NULL); 910 | 911 | // price & volume normalization 912 | lots = NormalizeLots(symbol, lots); 913 | price = NormalizePrice(symbol, price); 914 | slippage = NormalizePoints(symbol, slippage); 915 | 916 | // ECN brokers require order to be opened before sl/tp is specified 917 | int ticket = OrderSend(symbol, orderType, lots, price, slippage, 0, 0, comment); 918 | if (ticket < 0) { 919 | sendError(GetLastError(), "Failed to send order."); 920 | return; 921 | } 922 | else { 923 | // order created; now select it 924 | if (!OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES)) { 925 | // order was not found in Trades tab; check Account History tab in case it already closed 926 | if (OrderSelect(ticket, SELECT_BY_TICKET, MODE_HISTORY)) { 927 | // order is closed; return it with a warning 928 | CJAVal newOrder; 929 | _getSelectedOrder(newOrder); 930 | sendResponse(newOrder, StringFormat("Order # %d was closed immediately after being opened.", ticket)); 931 | return; 932 | } 933 | else { 934 | // new order was not found; this should never happen unless there is a server error 935 | sendError(StringFormat("Order # %d was created, but is not found in the Trades or Account History tabs.", ticket)); 936 | return; 937 | } 938 | } 939 | else { 940 | // order is open/pending and is selected 941 | // add sl/tp if necessary 942 | if (_modifySelectedOrder(NULL, stopLoss, takeProfit, stopLossPoints, takeProfitPoints)) { 943 | // reselect the order and send it back to client 944 | sendOrder(ticket); 945 | return; 946 | } 947 | else { 948 | sendError(GetLastError(), "Order was created, but failed to set sl/tp."); 949 | return; 950 | } 951 | } 952 | } 953 | } 954 | 955 | void Do_OrderModify(CJAVal& req) { 956 | if (!assertParamExists(req, "ticket")) { 957 | return; 958 | } 959 | int ticket = (int)req["ticket"].ToInt(); 960 | 961 | // stop-loss and take-profit params must be either relative or absolute, but not both 962 | if (!IsNullOrMissing(req, "sl") && !IsNullOrMissing(req, "sl_points")) { 963 | sendError("Stop-loss cannot be both relative (sl_points) and absolute (sl). Specify one or the other."); 964 | return; 965 | } 966 | if (!IsNullOrMissing(req, "tp") && !IsNullOrMissing(req, "tp_points")) { 967 | sendError("Take-profit cannot be both relative (tp_points) and absolute (tp). Specify one or the other."); 968 | return; 969 | } 970 | 971 | if (!OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES)) { 972 | sendError(StringFormat("Order # %d not found.", ticket)); 973 | return; 974 | } 975 | else { 976 | // order selected 977 | // overwrite existing values with request params if provided 978 | double newPrice = GetDefault(req, "price", (double)NULL); 979 | double stopLoss = GetDefault(req, "sl", (double)NULL); 980 | double takeProfit = GetDefault(req, "tp", (double)NULL); 981 | int newStopLossPoints = GetDefault(req, "sl_points", (int)NULL); 982 | int newTakeProfitPoints = GetDefault(req, "tp_points", (int)NULL); 983 | 984 | if (_modifySelectedOrder(newPrice, stopLoss, takeProfit, newStopLossPoints, newTakeProfitPoints)) { 985 | // reselect the order and send it back to client 986 | sendOrder(ticket); 987 | return; 988 | } 989 | else { 990 | sendError(GetLastError(), StringFormat("Failed to modify order # %d.", ticket)); 991 | return; 992 | } 993 | } 994 | } 995 | 996 | // modifies a selected order, nudging the given values to obey the trading rules 997 | // see: https://book.mql4.com/appendix/limits 998 | bool _modifySelectedOrder(double price=NULL, double stopLoss=NULL, double takeProfit=NULL, int stopLossPoints=NULL, int takeProfitPoints=NULL) { 999 | if (price == NULL && stopLoss == NULL && takeProfit == NULL && stopLossPoints == NULL && takeProfitPoints == NULL) { 1000 | return true; 1001 | } 1002 | int ticket = OrderTicket(); 1003 | string symbol = OrderSymbol(); 1004 | int type = OrderType(); 1005 | 1006 | int stopLevelPoints = (int)MarketInfo(symbol, MODE_STOPLEVEL); 1007 | // +1 to deal with non-inclusive inequality 1008 | int freezeLevelPoints = (int)MarketInfo(symbol, MODE_FREEZELEVEL) + 1; 1009 | int minDistPoints = (int)MathMax(stopLevelPoints, (int)MathMax(freezeLevelPoints, MIN_POINT_DISTANCE)); 1010 | // round up just to be safe 1011 | double minDist = NormalizePriceUp(symbol, PointsToDouble(symbol, minDistPoints)); 1012 | double ask = MarketInfo(symbol, MODE_ASK); 1013 | double bid = MarketInfo(symbol, MODE_BID); 1014 | 1015 | if (IsPendingOrder(type) && price != NULL) { 1016 | price = NormalizePrice(symbol, _getPriceMod(type, ask, bid, minDist, price)); 1017 | } 1018 | else { 1019 | if (stopLoss == NULL && takeProfit == NULL && stopLossPoints == NULL && takeProfitPoints == NULL) { 1020 | // nothing left to do 1021 | return true; 1022 | } 1023 | price = OrderOpenPrice(); 1024 | } 1025 | 1026 | if (stopLossPoints != NULL) { 1027 | // relative value given 1028 | double dist = PointsToDouble(symbol, MathMax(stopLossPoints, minDistPoints)); 1029 | stopLoss = NormalizePrice(symbol, _getSLMod(type, ask, bid, dist, price)); 1030 | } 1031 | else if (stopLoss != NULL) { 1032 | // absolute value given 1033 | stopLoss = NormalizePrice(symbol, _getSLMod(type, ask, bid, minDist, price, stopLoss)); 1034 | } 1035 | else { 1036 | stopLoss = OrderStopLoss(); 1037 | } 1038 | 1039 | if (takeProfitPoints != NULL) { 1040 | // relative value given 1041 | double dist = PointsToDouble(symbol, MathMax(takeProfitPoints, minDistPoints)); 1042 | takeProfit = NormalizePrice(symbol, _getTPMod(type, ask, bid, dist, price)); 1043 | } 1044 | else if (takeProfit != NULL) { 1045 | // absolute value given 1046 | takeProfit = NormalizePrice(symbol, _getTPMod(type, ask, bid, minDist, price, takeProfit)); 1047 | } 1048 | else { 1049 | takeProfit = OrderTakeProfit(); 1050 | } 1051 | 1052 | return OrderModify(ticket, price, stopLoss, takeProfit, 0, CLR_NONE); 1053 | } 1054 | 1055 | // given a price, gets the mod price 1056 | // see: https://book.mql4.com/appendix/limits 1057 | double _getPriceMod(int orderType, double ask, double bid, double minDist, double price) { 1058 | if (orderType != OP_BUY && orderType != OP_SELL) { 1059 | if (orderType == OP_BUYLIMIT) { 1060 | price = MathMin(ask - minDist, price); 1061 | } 1062 | else if (orderType == OP_SELLLIMIT) { 1063 | price = MathMax(bid + minDist, price); 1064 | } 1065 | else if (orderType == OP_BUYSTOP) { 1066 | price = MathMax(ask + minDist, price); 1067 | } 1068 | else if (orderType == OP_SELLSTOP) { 1069 | price = MathMin(bid - minDist, price); 1070 | } 1071 | } 1072 | return price; 1073 | } 1074 | 1075 | // given an absolute stop-loss, gets the mod stop-loss 1076 | // see: https://book.mql4.com/appendix/limits 1077 | double _getSLMod(int orderType, double ask, double bid, double minDist, double price, double stopLoss) { 1078 | if (orderType == OP_BUY) { 1079 | if (bid - stopLoss < minDist) { 1080 | stopLoss = bid - minDist; 1081 | } 1082 | } 1083 | else if (orderType == OP_SELL) { 1084 | if (stopLoss - ask < minDist) { 1085 | stopLoss = ask + minDist; 1086 | } 1087 | } 1088 | else if (orderType == OP_BUYLIMIT) { 1089 | if (price - stopLoss < minDist) { 1090 | stopLoss = price - minDist; 1091 | } 1092 | } 1093 | else if (orderType == OP_SELLLIMIT) { 1094 | if (stopLoss - price < minDist) { 1095 | stopLoss = price + minDist; 1096 | } 1097 | } 1098 | else if (orderType == OP_BUYSTOP) { 1099 | if (price - stopLoss < minDist) { 1100 | stopLoss = price - minDist; 1101 | } 1102 | } 1103 | else if (orderType == OP_SELLSTOP) { 1104 | if (stopLoss - price < minDist) { 1105 | stopLoss = price + minDist; 1106 | } 1107 | } 1108 | return stopLoss; 1109 | } 1110 | 1111 | // given relative stop-loss points, gets the mod stop-loss 1112 | // see: https://book.mql4.com/appendix/limits 1113 | double _getSLMod(int orderType, double ask, double bid, double dist, double price) { 1114 | if (orderType == OP_BUY) { 1115 | return bid - dist; 1116 | } 1117 | else if (orderType == OP_SELL) { 1118 | return ask + dist; 1119 | } 1120 | else if (orderType == OP_BUYLIMIT) { 1121 | return price - dist; 1122 | } 1123 | else if (orderType == OP_SELLLIMIT) { 1124 | return price + dist; 1125 | } 1126 | else if (orderType == OP_BUYSTOP) { 1127 | return price - dist; 1128 | } 1129 | else if (orderType == OP_SELLSTOP) { 1130 | return price + dist; 1131 | } 1132 | else { 1133 | // should never happen 1134 | return price; 1135 | } 1136 | } 1137 | 1138 | // given an absolute take-profit, gets the mod take-profit 1139 | // see: https://book.mql4.com/appendix/limits 1140 | double _getTPMod(int orderType, double ask, double bid, double minDist, double price, double takeProfit) { 1141 | if (orderType == OP_BUY) { 1142 | if (takeProfit - bid < minDist) { 1143 | takeProfit = bid + minDist; 1144 | } 1145 | } 1146 | else if (orderType == OP_SELL) { 1147 | if (ask - takeProfit < minDist) { 1148 | takeProfit = ask - minDist; 1149 | } 1150 | } 1151 | else if (orderType == OP_BUYLIMIT) { 1152 | if (takeProfit - price < minDist) { 1153 | takeProfit = price + minDist; 1154 | } 1155 | } 1156 | else if (orderType == OP_SELLLIMIT) { 1157 | if (price - takeProfit < minDist) { 1158 | takeProfit = price - minDist; 1159 | } 1160 | } 1161 | else if (orderType == OP_BUYSTOP) { 1162 | if (takeProfit - price < minDist) { 1163 | takeProfit = price + minDist; 1164 | } 1165 | } 1166 | else if (orderType == OP_SELLSTOP) { 1167 | if (price - takeProfit < minDist) { 1168 | takeProfit = price - minDist; 1169 | } 1170 | } 1171 | return takeProfit; 1172 | } 1173 | 1174 | // given relative take-profit points, gets the mod take-profit 1175 | // see: https://book.mql4.com/appendix/limits 1176 | double _getTPMod(int orderType, double ask, double bid, double dist, double price) { 1177 | if (orderType == OP_BUY) { 1178 | return bid + dist; 1179 | } 1180 | else if (orderType == OP_SELL) { 1181 | return ask - dist; 1182 | } 1183 | else if (orderType == OP_BUYLIMIT) { 1184 | return price + dist; 1185 | } 1186 | else if (orderType == OP_SELLLIMIT) { 1187 | return price - dist; 1188 | } 1189 | else if (orderType == OP_BUYSTOP) { 1190 | return price + dist; 1191 | } 1192 | else if (orderType == OP_SELLSTOP) { 1193 | return price - dist; 1194 | } 1195 | else { 1196 | return price; 1197 | } 1198 | } 1199 | 1200 | void Do_OrderClose(CJAVal& req) { 1201 | if (!assertParamExists(req, "ticket")) { 1202 | return; 1203 | } 1204 | int ticket = (int)req["ticket"].ToInt(); 1205 | 1206 | if (!OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES)) { 1207 | sendError(StringFormat("Order # %d not found.", ticket)); 1208 | return; 1209 | } 1210 | else { 1211 | // order selected 1212 | // use default values if request params were not provided 1213 | double lots = GetDefault(req, "lots", OrderLots()); 1214 | double price = GetDefault(req, "price", DefaultClosePrice(OrderSymbol(), OrderType())); 1215 | int slippage = GetDefault(req, "slippage", DefaultSlippage(OrderSymbol())); 1216 | 1217 | // price & volume normalization 1218 | lots = NormalizeLots(OrderSymbol(), lots); 1219 | price = NormalizePrice(OrderSymbol(), price); 1220 | slippage = NormalizePoints(OrderSymbol(), slippage); 1221 | 1222 | // open order: close it 1223 | if (!OrderClose(ticket, lots, price, slippage)) { 1224 | // failure 1225 | int errorCode = GetLastError(); 1226 | sendError(errorCode, StringFormat("Failed to close order # %d.", ticket)); 1227 | return; 1228 | } 1229 | else { 1230 | // success 1231 | sendResponse(StringFormat("Closed order # %d", ticket)); 1232 | return; 1233 | } 1234 | } 1235 | } 1236 | 1237 | void Do_OrderDelete(CJAVal& req) { 1238 | if (!assertParamExists(req, "ticket")) { 1239 | return; 1240 | } 1241 | int ticket = (int)req["ticket"].ToInt(); 1242 | bool closeIfOpened = GetDefault(req, "close_if_opened", false); 1243 | 1244 | if (!OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES)) { 1245 | sendError(StringFormat("Order # %d not found.", ticket)); 1246 | return; 1247 | } 1248 | else { 1249 | // order selected 1250 | if (!OrderDelete(ticket)) { 1251 | // failure 1252 | int errorCode = GetLastError(); 1253 | // check if order is open and closing is requested 1254 | if (errorCode == ERR_INVALID_TICKET && !IsPendingOrder(OrderType()) && closeIfOpened) { 1255 | // attempt to close at market price 1256 | req["lots"] = OrderLots(); 1257 | req["price"] = DefaultClosePrice(OrderSymbol(), OrderType()); 1258 | req["slippage"] = DefaultSlippage(OrderSymbol()); 1259 | Do_OrderClose(req); 1260 | return; 1261 | } 1262 | sendError(errorCode, StringFormat("Failed to delete order # %d.", ticket)); 1263 | return; 1264 | } 1265 | else { 1266 | // success 1267 | sendResponse(StringFormat("Deleted pending order # %d.", ticket)); 1268 | return; 1269 | } 1270 | } 1271 | } 1272 | 1273 | void Run_Indicator(CJAVal& req) { 1274 | if (!assertParamExists(req, "indicator") || !assertParamExists(req, "argv") || !assertParamExists(req, "timeout")) { 1275 | return; 1276 | } 1277 | string strIndicator = req["indicator"].ToStr(); 1278 | CJAVal argv = req["argv"]; 1279 | long timeout = (int)req["timeout"].ToInt(); 1280 | 1281 | // parse indicator function name 1282 | Indicator indicator = (Indicator)-1; 1283 | indicator = StringToEnum(strIndicator, indicator); 1284 | 1285 | // need to poll, as data may not be immediately available 1286 | int delay = 100; // milliseconds 1287 | long maxTries = timeout / delay; 1288 | bool isDone = false; 1289 | double results = NULL; 1290 | for (int try = 0; try < maxTries && !isDone; try++) { 1291 | results = _runIndicator(indicator, argv); 1292 | 1293 | // check if function name was recognized 1294 | if (results == NULL) { 1295 | string errorStr = StringFormat("Indicator not recognized: %s.", strIndicator); 1296 | sendError(errorStr); 1297 | return; 1298 | } 1299 | 1300 | // check for errors 1301 | int errorCode = GetLastError(); 1302 | if (errorCode == ERR_HISTORY_WILL_UPDATED) { 1303 | // data not yet loaded; retry until timeout 1304 | isDone = false; 1305 | } 1306 | else if (errorCode != 0) { 1307 | // error occured during indicator run 1308 | sendError(errorCode); 1309 | return; 1310 | } 1311 | else { 1312 | isDone = true; 1313 | } 1314 | 1315 | if (!isDone) { 1316 | Sleep(delay); 1317 | } 1318 | } 1319 | if (!isDone) { 1320 | sendError("Timed out waiting for indicator data to load."); 1321 | return; 1322 | } 1323 | else { 1324 | sendResponse(results); 1325 | return; 1326 | } 1327 | } 1328 | 1329 | double _runIndicator(Indicator indicator, CJAVal& argv) { 1330 | switch(indicator) { 1331 | case iAC: return iAC(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt()); 1332 | case iAD: return iAD(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt()); 1333 | case iADX: return iADX(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt()); 1334 | case iAlligator: return iAlligator(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt(), (int)argv[6].ToInt(), (int)argv[7].ToInt(), (int)argv[8].ToInt(), (int)argv[9].ToInt(), (int)argv[10].ToInt(), (int)argv[11].ToInt()); 1335 | case iAO: return iAO(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt()); 1336 | case iATR: return iATR(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt()); 1337 | case iBearsPower: return iBearsPower(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt()); 1338 | case iBands: return iBands(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), argv[3].ToDbl(), (int)argv[4].ToInt(), (int)argv[5].ToInt(), (int)argv[6].ToInt(), (int)argv[7].ToInt()); 1339 | case iBullsPower: return iBullsPower(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt()); 1340 | case iCCI: return iCCI(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt()); 1341 | case iDeMarker: return iDeMarker(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt()); 1342 | case iEnvelopes: return iEnvelopes(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt(), argv[6].ToDbl(), (int)argv[7].ToInt(), (int)argv[8].ToInt()); 1343 | case iForce: return iForce(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt()); 1344 | case iFractals: return iFractals(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt()); 1345 | case iGator: return iGator(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt(), (int)argv[6].ToInt(), (int)argv[7].ToInt(), (int)argv[8].ToInt(), (int)argv[9].ToInt(), (int)argv[10].ToInt(), (int)argv[11].ToInt()); 1346 | case iIchimoku: return iIchimoku(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt(), (int)argv[6].ToInt()); 1347 | case iBWMFI: return iBWMFI(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt()); 1348 | case iMomentum: return iMomentum(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt()); 1349 | case iMFI: return iMFI(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt()); 1350 | case iMA: return iMA(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt(), (int)argv[6].ToInt()); 1351 | case iOsMA: return iOsMA(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt(), (int)argv[6].ToInt()); 1352 | case iMACD: return iMACD(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt(), (int)argv[6].ToInt(), (int)argv[7].ToInt()); 1353 | case iOBV: return iOBV(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt()); 1354 | case iSAR: return iSAR(argv[0].ToStr(), (int)argv[1].ToInt(), argv[2].ToDbl(), argv[3].ToDbl(), (int)argv[4].ToInt()); 1355 | case iRSI: return iRSI(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt()); 1356 | case iRVI: return iRVI(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt()); 1357 | case iStdDev: return iStdDev(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt(), (int)argv[6].ToInt()); 1358 | case iStochastic: return iStochastic(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt(), (int)argv[4].ToInt(), (int)argv[5].ToInt(), (int)argv[6].ToInt(), (int)argv[7].ToInt(), (int)argv[8].ToInt()); 1359 | case iWPR: return iWPR(argv[0].ToStr(), (int)argv[1].ToInt(), (int)argv[2].ToInt(), (int)argv[3].ToInt()); 1360 | default: return NULL; 1361 | } 1362 | } 1363 | 1364 | bool IsValidTradeOperation(int orderType) { 1365 | return (orderType == OP_BUY || orderType == OP_SELL || orderType == OP_BUYLIMIT || orderType == OP_BUYSTOP || 1366 | orderType == OP_SELLLIMIT || orderType == OP_SELLSTOP); 1367 | } 1368 | 1369 | // current market open price, normalized to tick size 1370 | double DefaultOpenPrice(string symbol, int orderType) { 1371 | double price = (orderType == OP_BUY || orderType == OP_BUYLIMIT || orderType == OP_BUYSTOP) ? 1372 | MarketInfo(symbol, MODE_ASK) : MarketInfo(symbol, MODE_BID); 1373 | return NormalizePrice(symbol, price); 1374 | } 1375 | 1376 | // current market close price, normalized to tick size 1377 | double DefaultClosePrice(string symbol, int orderType) { 1378 | double price = (orderType == OP_BUY || orderType == OP_BUYLIMIT || orderType == OP_BUYSTOP) ? 1379 | MarketInfo(symbol, MODE_BID) : MarketInfo(symbol, MODE_ASK); 1380 | return NormalizePrice(symbol, price); 1381 | } 1382 | 1383 | // a permissive slippage value: double the spread, in tick size 1384 | int DefaultSlippage(string symbol) { 1385 | double tickSize = MarketInfo(symbol, MODE_TICKSIZE); 1386 | double spread = MathAbs(MarketInfo(symbol, MODE_ASK) - MarketInfo(symbol, MODE_BID)); 1387 | return int(2.0 * spread / tickSize); 1388 | } 1389 | 1390 | // price normalized to tick size; required for prices and sl/tp 1391 | double NormalizePrice(string symbol, double price) { 1392 | double tickSize = MarketInfo(symbol, MODE_TICKSIZE); 1393 | return MathRound(price / tickSize) * tickSize; 1394 | } 1395 | 1396 | // price normalized to tick size, rounded up; required for stoplevel/freezelevel min-distance calculation 1397 | double NormalizePriceUp(string symbol, double price) { 1398 | double tickSize = MarketInfo(symbol, MODE_TICKSIZE); 1399 | return MathCeil(price / tickSize) * tickSize; 1400 | } 1401 | 1402 | // points normalized to tick size; required for slippage calculation 1403 | int NormalizePoints(string symbol, int points) { 1404 | double pointsPerTick = MarketInfo(symbol, MODE_TICKSIZE) / MarketInfo(symbol, MODE_POINT); 1405 | return int(MathRound(points / pointsPerTick) * pointsPerTick); 1406 | } 1407 | 1408 | // volume, normalized to lot step 1409 | double NormalizeLots(string symbol, double lots) { 1410 | double lotStep = MarketInfo(symbol, MODE_LOTSTEP); 1411 | double minLot = MarketInfo(symbol, MODE_MINLOT); 1412 | lots = MathRound(lots / lotStep) * lotStep; 1413 | return MathMax(lots, minLot); 1414 | } 1415 | 1416 | double PointsToDouble(string symbol, int points) { 1417 | return points * MarketInfo(symbol, MODE_POINT); 1418 | } 1419 | 1420 | int DoubleToPoints(string symbol, double val) { 1421 | return (int) MathRound(val / MarketInfo(symbol, MODE_POINT)); 1422 | } 1423 | 1424 | string BoolToString(bool val) { 1425 | return val ? "true" : "false"; 1426 | } 1427 | 1428 | string LongToString(long val) { 1429 | return DoubleToStr(val, 0); 1430 | } 1431 | 1432 | template T StringToEnum(string str, T enumType) { 1433 | for (int i = 0; i < 256; i++) { 1434 | if (str == EnumToString(enumType = (T)i)) { 1435 | return(enumType); 1436 | } 1437 | } 1438 | return -1; 1439 | } 1440 | 1441 | template void ArrayErase(T& arr[], int index) { 1442 | int last; 1443 | for(last = ArraySize(arr) - 1; index < last; ++index) { 1444 | arr[index] = arr[index + 1]; 1445 | } 1446 | ArrayResize(arr, last); 1447 | } 1448 | 1449 | template bool ArrayEraseElement(T& arr[], E element) { 1450 | bool elementRemoved = false; 1451 | for (int i = 0; i < ArraySize(arr); ++i) { 1452 | if (arr[i] == element) { 1453 | int index = i; 1454 | int last; 1455 | for(last = ArraySize(arr) - 1; index < last; ++index) { 1456 | arr[index] = arr[index + 1]; 1457 | } 1458 | ArrayResize(arr, last); 1459 | elementRemoved = true; 1460 | --i; 1461 | } 1462 | } 1463 | return elementRemoved; 1464 | } 1465 | 1466 | bool ArrayContains(CJAVal& arr, string val) { 1467 | for (int i = arr.Size() - 1; i >= 0; --i) { 1468 | string curVal = arr[i].ToStr(); 1469 | if (val == curVal) { 1470 | return true; 1471 | } 1472 | } 1473 | return false; 1474 | } 1475 | 1476 | void Trace(string msg) { 1477 | if (VERBOSE) { 1478 | Print(msg); 1479 | } 1480 | } 1481 | 1482 | bool IsNullOrMissing(CJAVal& obj, string key) { 1483 | if (obj.HasKey(key)) { 1484 | return (obj[key].m_type == jtNULL); 1485 | } 1486 | return true; 1487 | } 1488 | 1489 | bool IsPendingOrder(int orderType) { 1490 | return (orderType == OP_BUYLIMIT || orderType == OP_BUYSTOP || orderType == OP_SELLLIMIT || orderType == OP_SELLSTOP); 1491 | } 1492 | 1493 | bool GetDefault(CJAVal& obj, string key, bool defaultVal) { 1494 | if (!IsNullOrMissing(obj, key)) { 1495 | return obj[key].ToBool(); 1496 | } 1497 | return defaultVal; 1498 | } 1499 | 1500 | int GetDefault(CJAVal& obj, string key, int defaultVal) { 1501 | return (int)GetDefault(obj, key, (long)defaultVal); 1502 | } 1503 | 1504 | long GetDefault(CJAVal& obj, string key, long defaultVal) { 1505 | if (!IsNullOrMissing(obj, key)) { 1506 | return obj[key].ToInt(); 1507 | } 1508 | return defaultVal; 1509 | } 1510 | 1511 | double GetDefault(CJAVal& obj, string key, double defaultVal) { 1512 | if (!IsNullOrMissing(obj, key)) { 1513 | return obj[key].ToDbl(); 1514 | } 1515 | return defaultVal; 1516 | } 1517 | 1518 | string GetDefault(CJAVal& obj, string key, string defaultVal) { 1519 | if (!IsNullOrMissing(obj, key)) { 1520 | return obj[key].ToStr(); 1521 | } 1522 | return defaultVal; 1523 | } 1524 | 1525 | bool IsRunning() { 1526 | return !IsStopped() && context != NULL; 1527 | } 1528 | -------------------------------------------------------------------------------- /metatrader4/config/metaeditor.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoeJoder/metatrader4-server/fc3ab36d6824eade1826c2a81ae06e78cf92006f/metatrader4/config/metaeditor.ini -------------------------------------------------------------------------------- /metatrader4/config/zeromq_server_startup.template.ini: -------------------------------------------------------------------------------- 1 | ; Startup settings for MetaTrader 4 2 | ; Connects to an account and launches a chart with the `ZeroMQ Server` attached. 3 | ; See: https://www.metatrader4.com/en/trading-platform/help/service/start_conf_file 4 | 5 | ; put account login info here and uncomment the lines 6 | #Login= 7 | #Password= 8 | #Server= 9 | 10 | AutoConfiguration=true 11 | EnableDDE=true 12 | EnableNews=false 13 | 14 | ; open chart and run expert and/or script 15 | Symbol=EURUSD 16 | Period=H1 17 | Template=Popular.tpl 18 | Script=ZeroMQ_Server 19 | ;ScriptParameters=ZeroMQ_Server.set 20 | 21 | ; experts settings 22 | ExpertsEnable=true 23 | ExpertsDllImport=true 24 | ExpertsExpImport=true 25 | ExpertsTrades=true 26 | 27 | -------------------------------------------------------------------------------- /metatrader4/restart_mt4.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem title: restart_mt4.bat 4 | rem description: A dev script for restarting the MetaTrader 4 terminal with a clean state. 5 | rem MT4 is shutdown gracefully, its chart cache is cleared, then MT4 is restarted 6 | rem with the given INI configuration. 7 | rem precondition: This script is run in a Windows instance which has powershell.exe on the PATH, and 8 | rem MetaTrader 4 (32-bit) installed and possibly running. 9 | rem postcondition: MT4 will be running with a single chart which has `ZeroMQ Server` attached (see remote_deploy.sh), 10 | rem ready to receive client requests via ZeroMQ. 11 | rem 12 | rem configurable variables: 13 | rem --------------------------------------------------------------------------- 14 | 15 | rem the MT4 profile 16 | set MT4_PROFILE=C:\Users\Joe\AppData\Roaming\MetaQuotes\Terminal\B4465D2178C9DCD8E8CD3CCFEA8AA766 17 | 18 | rem the MT4 startup configuration (relative to MT4_PROFILE) 19 | set START_INI=config\zeromq_server_startup.ini 20 | 21 | rem the MT4 installation 22 | set MT4_HOME=C:\Program Files (x86)\LMFX MetaTrader 4 Terminal 23 | 24 | rem --------------------------------------------------------------------------- 25 | 26 | set MT4_EXE=%MT4_HOME%\terminal.exe 27 | set MT4_CACHED_CHARTS=%MT4_PROFILE%\profiles\default 28 | 29 | echo Attempting graceful shutdown of MT4... 30 | powershell.exe "Get-Process | Where-Object {$_.Path -eq \"%MT4_EXE%\"} | foreach {$_.CloseMainWindow()}" 31 | 32 | echo Clearing cached charts... 33 | powershell.exe "Remove-Item -path \"%MT4_CACHED_CHARTS%\*.chr\"" 34 | 35 | echo Starting up MT4... 36 | powershell.exe "Start-Process -FilePath \"%MT4_EXE%\" -WorkingDirectory \"%MT4_HOME%\" -ArgumentList \"%START_INI%\"" 37 | 38 | -------------------------------------------------------------------------------- /metatrader4/templates/Popular.tpl: -------------------------------------------------------------------------------- 1 | 2 | symbol=EURUSD 3 | period=30 4 | leftpos=408 5 | digits=4 6 | scale=8 7 | graph=1 8 | fore=1 9 | grid=1 10 | volume=0 11 | scroll=0 12 | shift=1 13 | ohlc=1 14 | askline=0 15 | days=1 16 | descriptions=0 17 | shift_size=20 18 | fixed_pos=0 19 | window_left=307 20 | window_top=159 21 | window_right=614 22 | window_bottom=318 23 | window_type=3 24 | background_color=0 25 | foreground_color=16777215 26 | barup_color=65280 27 | bardown_color=65280 28 | bullcandle_color=0 29 | bearcandle_color=16777215 30 | chartline_color=65280 31 | volumes_color=3329330 32 | grid_color=10061943 33 | askline_color=255 34 | stops_color=255 35 | 36 | 37 | height=148 38 | 39 | name=main 40 | 41 | 42 | 43 | 44 | height=37 45 | 46 | name=MACD 47 | fast_ema=12 48 | slow_ema=26 49 | macd_sma=9 50 | apply=0 51 | color=3329330 52 | style=0 53 | weight=1 54 | signal_color=255 55 | signal_style=0 56 | signal_weight=1 57 | levels_color=12632256 58 | levels_style=2 59 | levels_weight=1 60 | level_0=0.0000 61 | period_flags=0 62 | show_data=1 63 | 64 | 65 | 66 | 67 | height=35 68 | 69 | name=Stochastic Oscillator 70 | kperiod=8 71 | dperiod=3 72 | slowing=3 73 | method=0 74 | apply=0 75 | color=3329330 76 | style=0 77 | weight=1 78 | color2=255 79 | style2=0 80 | weight2=1 81 | min=0.000000 82 | max=100.000000 83 | levels_color=12632256 84 | levels_style=2 85 | levels_weight=1 86 | level_0=20.0000 87 | level_1=80.0000 88 | period_flags=0 89 | show_data=1 90 | 91 | 92 | 93 | 94 | height=50 95 | 96 | name=Relative Strength Index 97 | period=13 98 | apply=0 99 | color=3329330 100 | style=0 101 | weight=1 102 | min=0.000000 103 | max=100.000000 104 | levels_color=12632256 105 | levels_style=2 106 | levels_weight=1 107 | level_0=30.0000 108 | level_1=70.0000 109 | period_flags=0 110 | show_data=1 111 | 112 | 113 | name=Relative Strength Index 114 | period=3 115 | apply=0 116 | color=255 117 | style=0 118 | weight=1 119 | min=0.000000 120 | max=100.000000 121 | levels_color=12632256 122 | levels_style=2 123 | levels_weight=1 124 | level_0=30.0000 125 | level_1=70.0000 126 | period_flags=0 127 | show_data=1 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /remote_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## title: remote_deploy.sh 4 | ## description: A dev script for deploying & compiling the `ZeroMQ Server` script and its dependencies from a 5 | ## local dev environment into a remote MetaTrader 4 instance via SSH. 6 | ## precondition: Deployment target is a Windows 10 instance which has MT4 (32-bit) installed, and 7 | ## WSL (Windows Subsystem for Linux) installed which is running a SSH server. The client machine running 8 | ## this script should have SSH master connections enabled to prevent excessive handshakes. 9 | ## postcondition: MetaTrader 4 will have the latest server script and should be restarted to apply it 10 | ## (see restart_mt4.bat). 11 | ## 12 | ## configurable variables: 13 | ## ---------------------------------------------------------------------------- 14 | 15 | # the server script filename 16 | SCRIPT_FILENAME='ZeroMQ_Server.mq4' 17 | 18 | # the SSH endpoint of the deployment target 19 | SSH_ENDPOINT='joe@win10' 20 | 21 | # the remote MT4 profile location 22 | MT4_PROFILE='C:\Users\Joe\AppData\Roaming\MetaQuotes\Terminal\B4465D2178C9DCD8E8CD3CCFEA8AA766' 23 | 24 | # the remote MT4 install location 25 | MT4_HOME='C:\Program Files (x86)\LMFX MetaTrader 4 Terminal' 26 | 27 | ## ---------------------------------------------------------------------------- 28 | 29 | # closes master connection and exits with code 30 | function closeAndExit() { 31 | ssh -O exit "$SSH_ENDPOINT" &> /dev/null 32 | exit $1 33 | } 34 | 35 | echo "Deploying \"$SCRIPT_FILENAME\"..." 36 | 37 | # start master connection 38 | ssh -Nf "$SSH_ENDPOINT" 39 | 40 | # Windows paths used by WSL must be converted into Linux format 41 | WSL_MT4_PROFILE="$(ssh "$SSH_ENDPOINT" "echo \$(wslpath '$MT4_PROFILE')")" 42 | WSL_MT4_HOME="$(ssh "$SSH_ENDPOINT" "echo \$(wslpath '$MT4_HOME')")" 43 | SCRIPT_DIR="$(dirname "$(realpath -s "$0")")" 44 | WIN_COMPILER_LOG="$MT4_PROFILE\\logs\\compiler.log" 45 | WSL_COMPILER_LOG="$(ssh "$SSH_ENDPOINT" "echo \$(wslpath '$WIN_COMPILER_LOG')")" 46 | 47 | if ssh "$SSH_ENDPOINT" "[ ! -d \"$WSL_MT4_PROFILE\" ]"; then 48 | echo "[ERROR] MetaTrader profile not found. Deployment aborted." 49 | closeAndExit 1 50 | fi 51 | 52 | rsync -avh "$SCRIPT_DIR/mql-zmq/Include/" "$SSH_ENDPOINT:\"$WSL_MT4_PROFILE/MQL4/Include/\"" 53 | rsync -avh "$SCRIPT_DIR/mql-zmq/Library/MT4/" "$SSH_ENDPOINT:\"$WSL_MT4_PROFILE/MQL4/Libraries/\"" 54 | rsync -avh --exclude "MQL4/Include/json/README.md" --exclude "config/zeromq_server_startup.template.ini" "$SCRIPT_DIR/metatrader4/" "$SSH_ENDPOINT:\"$WSL_MT4_PROFILE/\"" 55 | 56 | # compile the server script 57 | if ssh "$SSH_ENDPOINT" "[ ! -f \"$WSL_MT4_HOME/metaeditor.exe\" ]"; then 58 | echo "[WARNING] The MQL4 compiler was not found. Skipping compilation..." 59 | closeAndExit 1 60 | fi 61 | 62 | echo -e "\nCompiling..." 63 | if ssh "$SSH_ENDPOINT" " 64 | rm \"$WSL_COMPILER_LOG\" &> /dev/null 65 | cd \"$WSL_MT4_HOME\" 66 | ./metaeditor.exe /log:\"$WIN_COMPILER_LOG\" /compile:\"$MT4_PROFILE\\MQL4\\Scripts\\$SCRIPT_FILENAME\" 67 | [ ! -f \"$WSL_COMPILER_LOG\" ]"; then 68 | echo "[ERROR] Compiler log not found." 69 | closeAndExit 1 70 | fi 71 | 72 | # convert log to UTF-8 and direct any warnings/errors to local STDOUT 73 | ssh "$SSH_ENDPOINT" "iconv -f utf-16 -t utf-8 \"$WSL_COMPILER_LOG\"" | grep -E --color=auto "warning|error" 74 | closeAndExit 0 75 | --------------------------------------------------------------------------------