Merge pull request #6 from freedumb2000/ticks-support

Support for tick data
This commit is contained in:
Nikolai
2020-01-28 23:30:33 +03:00
committed by GitHub
3 changed files with 392 additions and 376 deletions
Binary file not shown.
+385 -376
View File
@@ -1,376 +1,385 @@
# Metaquotes MQL5 - JSON - API # Metaquotes MQL5 - JSON - API
### Development state: first stable release ### Development state: first stable release
Tested on macOS Mojave / Windows 10 in Parallels Desktop container. Tested on macOS Mojave / Windows 10 in Parallels Desktop container.
Working in production on Debian 10 / Wine 4. Working in production on Debian 10 / Wine 4.
An issue was found because of REP/REQ socket. Architecture changes are in development. An issue was found because of REP/REQ socket. Architecture changes are in development.
## Table of Contents ## Table of Contents
* [About the Project](#about-the-project)
* [Installation](#installation) - [About the Project](#about-the-project)
* [Documentation](#documentation) - [Installation](#installation)
* [Usage](#usage) - [Documentation](#documentation)
* [Live data and streaming events](#live-data-and-streaming-events) - [Usage](#usage)
* [Error handling](#error-handling) - [Live data and streaming events](#live-data-and-streaming-events)
* [License](#license) - [Error handling](#error-handling)
- [License](#license)
## About the Project
## About the Project
This project was developed to work as a server for the Backtrader Python trading framework. It is based on ZeroMQ sockets and uses JSON format to communicate. But now it has grown to the independent project. You can use it with any programming language that has [ZeroMQ binding](http://zeromq.org/bindings:_start).
This project was developed to work as a server for the Backtrader Python trading framework. It is based on ZeroMQ sockets and uses JSON format to communicate. But now it has grown to the independent project. You can use it with any programming language that has [ZeroMQ binding](http://zeromq.org/bindings:_start).
Backtrader Python client is located here: [Python Backtrader - Metaquotes MQL5 ](https://github.com/khramkov/Backtrader-MQL5-API) Backtrader Python client is located here: [Python Backtrader - Metaquotes MQL5 ](https://github.com/khramkov/Backtrader-MQL5-API)
In development: In development:
* Devitation
* Stop limit orders - Devitation
- Stop limit orders
## Installation
## Installation
1. Install ZeroMQ for MQL5 [https://github.com/dingmaotu/mql-zmq](https://github.com/dingmaotu/mql-zmq)
2. Put `include/Json.mqh` from this repo to your MetaEditor `include` directoty. 1. Install ZeroMQ for MQL5 [https://github.com/dingmaotu/mql-zmq](https://github.com/dingmaotu/mql-zmq)
3. Download and compile `experts/JsonAPI.mq5` script. 2. Put `include/Json.mqh` from this repo to your MetaEditor `include` directoty.
4. Check if Metatrader 5 automatic trading is allowed. 3. Download and compile `experts/JsonAPI.mq5` script.
5. Attach the script to a chart in Metatrader 5. 4. Check if Metatrader 5 automatic trading is allowed.
6. Allow DLL import in dialog window. 5. Attach the script to a chart in Metatrader 5.
7. Check if the ports are free to use. (default:`15555`,`15556`, `15557`,`15558`) 6. Allow DLL import in dialog window.
7. Check if the ports are free to use. (default:`15555`,`15556`, `15557`,`15558`)
## Documentation
## Documentation
The script uses four ZeroMQ sockets:
The script uses four ZeroMQ sockets:
1. `System socket` - recives requests from client and replies 'OK'
2. `Data socket` - pushes data to client depending on the request via System socket. 1. `System socket` - recives requests from client and replies 'OK'
3. `Live socket` - automatically pushes last candle when it closes. 2. `Data socket` - pushes data to client depending on the request via System socket.
4. `Streaming socket` - automatically pushes last transaction info every time it happens. 3. `Live socket` - automatically pushes last candle when it closes.
4. `Streaming socket` - automatically pushes last transaction info every time it happens.
The idea is to send requests via `System socket` and recieve results/errors via `Data socket`. Event handlers should be created for `Live socket` and `Streaming socket` because the server sends data to theese sockets automatically. See examples in [Live data and streaming events](#live-data-and-streaming-events) section.
The idea is to send requests via `System socket` and recieve results/errors via `Data socket`. Event handlers should be created for `Live socket` and `Streaming socket` because the server sends data to theese sockets automatically. See examples in [Live data and streaming events](#live-data-and-streaming-events) section.
`System socket` request uses default JSON dictionary:
`System socket` request uses default JSON dictionary:
```
{ ```
"action": null, {
"actionType": null, "action": null,
"symbol": null, "actionType": null,
"chartTF": null, "symbol": null,
"fromDate": null, "chartTF": null,
"toDate": null, "fromDate": null,
"id": null, "toDate": null,
"magic": null, "id": null,
"volume": null, "magic": null,
"price": null, "volume": null,
"stoploss": null, "price": null,
"takeprofit": null, "stoploss": null,
"expiration": null, "takeprofit": null,
"deviation": null, "expiration": null,
"comment": null "deviation": null,
} "comment": null
``` }
Check out the available combinations of `action` and `actionType`: ```
action | actionType | Description | Check out the available combinations of `action` and `actionType`:
-----------|----------------------|----------------------------|
CONFIG | null | Set script configuration | | action | actionType | Description |
ACCOUNT | null | Get account settings | | --------- | --------------------- | ---------------------------- |
BALANCE | null | Get current balance | | CONFIG | null | Set script configuration |
POSITIONS | null | Get current open positions | | ACCOUNT | null | Get account settings |
ORDERS | null | Get current open orders | | BALANCE | null | Get current balance |
HISTORY | DATA | Get data history | | POSITIONS | null | Get current open positions |
HISTORY | TRADES | Get trades history | | ORDERS | null | Get current open orders |
TRADE | ORDER_TYPE_BUY | Buy market | | HISTORY | DATA | Get data history |
TRADE | ORDER_TYPE_SELL | Sell market | | HISTORY | TRADES | Get trades history |
TRADE | ORDER_TYPE_BUY_LIMIT | Buy limit | | HISTORY | WRITE | Download history data as CSV |
TRADE | ORDER_TYPE_SELL_LIMIT| Sell limit | | TRADE | ORDER_TYPE_BUY | Buy market |
TRADE | ORDER_TYPE_BUY_STOP | Buy stop | | TRADE | ORDER_TYPE_SELL | Sell market |
TRADE | ORDER_TYPE_SELL_STOP | Sell stop | | TRADE | ORDER_TYPE_BUY_LIMIT | Buy limit |
TRADE | POSITION_MODIFY | Position modify | | TRADE | ORDER_TYPE_SELL_LIMIT | Sell limit |
TRADE | POSITION_PARTIAL | Position close partial | | TRADE | ORDER_TYPE_BUY_STOP | Buy stop |
TRADE | POSITION_CLOSE_ID | Position close by id | | TRADE | ORDER_TYPE_SELL_STOP | Sell stop |
TRADE | POSITION_CLOSE_SYMBOL| Positions close by symbol | | TRADE | POSITION_MODIFY | Position modify |
TRADE | ORDER_MODIFY | Order modify | | TRADE | POSITION_PARTIAL | Position close partial |
TRADE | ORDER_CANCEL | Order cancel | | TRADE | POSITION_CLOSE_ID | Position close by id |
| TRADE | POSITION_CLOSE_SYMBOL | Positions close by symbol |
Python 3 API class example: | TRADE | ORDER_MODIFY | Order modify |
| TRADE | ORDER_CANCEL | Order cancel |
``` python
import zmq Python 3 API class example:
class MTraderAPI: ```python
def __init__(self, host=None): import zmq
self.HOST = host or 'localhost'
self.SYS_PORT = 15555 # REP/REQ port class MTraderAPI:
self.DATA_PORT = 15556 # PUSH/PULL port def __init__(self, host=None):
self.LIVE_PORT = 15557 # PUSH/PULL port self.HOST = host or 'localhost'
self.EVENTS_PORT = 15558 # PUSH/PULL port self.SYS_PORT = 15555 # REP/REQ port
self.DATA_PORT = 15556 # PUSH/PULL port
# ZeroMQ timeout in seconds self.LIVE_PORT = 15557 # PUSH/PULL port
sys_timeout = 1 self.EVENTS_PORT = 15558 # PUSH/PULL port
data_timeout = 10
# ZeroMQ timeout in seconds
# initialise ZMQ context sys_timeout = 1
context = zmq.Context() data_timeout = 10
# connect to server sockets # initialise ZMQ context
try: context = zmq.Context()
self.sys_socket = context.socket(zmq.REQ)
# set port timeout # connect to server sockets
self.sys_socket.RCVTIMEO = sys_timeout * 1000 try:
self.sys_socket.connect('tcp://{}:{}'.format(self.HOST, self.SYS_PORT)) self.sys_socket = context.socket(zmq.REQ)
# set port timeout
self.data_socket = context.socket(zmq.PULL) self.sys_socket.RCVTIMEO = sys_timeout * 1000
# set port timeout self.sys_socket.connect('tcp://{}:{}'.format(self.HOST, self.SYS_PORT))
self.data_socket.RCVTIMEO = data_timeout * 1000
self.data_socket.connect('tcp://{}:{}'.format(self.HOST, self.DATA_PORT)) self.data_socket = context.socket(zmq.PULL)
except zmq.ZMQError: # set port timeout
raise zmq.ZMQBindError("Binding ports ERROR") self.data_socket.RCVTIMEO = data_timeout * 1000
self.data_socket.connect('tcp://{}:{}'.format(self.HOST, self.DATA_PORT))
def _send_request(self, data: dict) -> None: except zmq.ZMQError:
"""Send request to server via ZeroMQ System socket""" raise zmq.ZMQBindError("Binding ports ERROR")
try:
self.sys_socket.send_json(data) def _send_request(self, data: dict) -> None:
msg = self.sys_socket.recv_string() """Send request to server via ZeroMQ System socket"""
# terminal received the request try:
assert msg == 'OK', 'Something wrong on server side' self.sys_socket.send_json(data)
except AssertionError as err: msg = self.sys_socket.recv_string()
raise zmq.NotDone(err) # terminal received the request
except zmq.ZMQError: assert msg == 'OK', 'Something wrong on server side'
raise zmq.NotDone("Sending request ERROR") except AssertionError as err:
raise zmq.NotDone(err)
def _pull_reply(self): except zmq.ZMQError:
"""Get reply from server via Data socket with timeout""" raise zmq.NotDone("Sending request ERROR")
try:
msg = self.data_socket.recv_json() def _pull_reply(self):
except zmq.ZMQError: """Get reply from server via Data socket with timeout"""
raise zmq.NotDone('Data socket timeout ERROR') try:
return msg msg = self.data_socket.recv_json()
except zmq.ZMQError:
def live_socket(self, context=None): raise zmq.NotDone('Data socket timeout ERROR')
"""Connect to socket in a ZMQ context""" return msg
try:
context = context or zmq.Context.instance() def live_socket(self, context=None):
socket = context.socket(zmq.PULL) """Connect to socket in a ZMQ context"""
socket.connect('tcp://{}:{}'.format(self.HOST, self.LIVE_PORT)) try:
except zmq.ZMQError: context = context or zmq.Context.instance()
raise zmq.ZMQBindError("Live port connection ERROR") socket = context.socket(zmq.PULL)
return socket socket.connect('tcp://{}:{}'.format(self.HOST, self.LIVE_PORT))
except zmq.ZMQError:
def streaming_socket(self, context=None): raise zmq.ZMQBindError("Live port connection ERROR")
"""Connect to socket in a ZMQ context""" return socket
try:
context = context or zmq.Context.instance() def streaming_socket(self, context=None):
socket = context.socket(zmq.PULL) """Connect to socket in a ZMQ context"""
socket.connect('tcp://{}:{}'.format(self.HOST, self.EVENTS_PORT)) try:
except zmq.ZMQError: context = context or zmq.Context.instance()
raise zmq.ZMQBindError("Data port connection ERROR") socket = context.socket(zmq.PULL)
return socket socket.connect('tcp://{}:{}'.format(self.HOST, self.EVENTS_PORT))
except zmq.ZMQError:
def construct_and_send(self, **kwargs) -> dict: raise zmq.ZMQBindError("Data port connection ERROR")
"""Construct a request dictionary from default and send it to server""" return socket
# default dictionary def construct_and_send(self, **kwargs) -> dict:
request = { """Construct a request dictionary from default and send it to server"""
"action": None,
"actionType": None, # default dictionary
"symbol": None, request = {
"chartTF": None, "action": None,
"fromDate": None, "actionType": None,
"toDate": None, "symbol": None,
"id": None, "chartTF": None,
"magic": None, "fromDate": None,
"volume": None, "toDate": None,
"price": None, "id": None,
"stoploss": None, "magic": None,
"takeprofit": None, "volume": None,
"expiration": None, "price": None,
"deviation": None, "stoploss": None,
"comment": None "takeprofit": None,
} "expiration": None,
"deviation": None,
# update dict values if exist "comment": None
for key, value in kwargs.items(): }
if key in request:
request[key] = value # update dict values if exist
else: for key, value in kwargs.items():
raise KeyError('Unknown key in **kwargs ERROR') if key in request:
request[key] = value
# send dict to server else:
self._send_request(request) raise KeyError('Unknown key in **kwargs ERROR')
# return server reply # send dict to server
return self._pull_reply() self._send_request(request)
```
## Usage # return server reply
All examples will be on Python 3. Lets create an instance of MetaTrader API class: return self._pull_reply()
```
``` python
api = MTraderAPI() ## Usage
```
All examples will be on Python 3. Lets create an instance of MetaTrader API class:
First of all we should configure the script `symbol` and `timeframe`. Live data stream will be configured to the seme params.
```python
``` python api = MTraderAPI()
rep = api.construct_and_send(action="CONFIG", symbol="EURUSD", chartTF="M5") ```
print(rep)
``` First of all we should configure the script `symbol` and `timeframe`. Live data stream will be configured to the same params.
Get information about the trading account. ```python
rep = api.construct_and_send(action="CONFIG", symbol="EURUSD", chartTF="M5")
``` python print(rep)
rep = api.construct_and_send(action="ACCOUNT") ```
print(rep)
``` Get information about the trading account.
Get historical data. `fromDate` should be in timestamp format. The data will be loaded to the last candle if `toDate` is `None`. Notice, that the script sends the last unclosed candle too. You should delete it manually. ```python
rep = api.construct_and_send(action="ACCOUNT")
There are some issues: print(rep)
```
- MetaTrader keeps historical data in cache. But when you make a request for the first time, MetaTrader downloads the data from a broker. This operation can exceed `Data socket` timeout. It depends on your broker. Second request will be handeled quickly.
- It takes 6-7 seconds to process `50000` M1 candles. It was tested on Windows 10 in Parallels Desktop container with 4 cores and 4GB RAM. So if you need more data there are three ways to handle it. 1) Increase `Data socket` timeout. 2) You can load data partially using `fromDate` and `toDate`. 3) You can use more powerfull hardware. Get historical data. `fromDate` should be in timestamp format. The data will be loaded to the last candle if `toDate` is `None`. Notice, that the script sends the last unclosed candle too. You should delete it manually.
``` python There are some issues:
rep = api.construct_and_send(action="HISTORY", actionType="DATA", symbol="EURUSD", chartTF="M5", fromDate=1555555555)
print(rep) - MetaTrader keeps historical data in cache. But when you make a request for the first time, MetaTrader downloads the data from a broker. This operation can exceed `Data socket` timeout. It depends on your broker. Second request will be handeled quickly.
``` - It takes 6-7 seconds to process `50000` M1 candles. It was tested on Windows 10 in Parallels Desktop container with 4 cores and 4GB RAM. So if you need more data there are three ways to handle it. 1) Increase `Data socket` timeout. 2) You can load data partially using `fromDate` and `toDate`. 3) You can use more powerfull hardware.
History data reply example: ```python
rep = api.construct_and_send(action="HISTORY", actionType="DATA", symbol="EURUSD", chartTF="M5", fromDate=1555555555)
``` print(rep)
{'data': [[1560782340, 1.12271, 1.12288, 1.12269, 1.12277, 46.0],[1560782400, 1.12278, 1.12299, 1.12276, 1.12297, 43.0],[1560782460, 1.12296, 1.12302, 1.12293, 1.123, 23.0]]} ```
```
History data reply example:
Buy market order.
```
``` python {'data': [[1560782340, 1.12271, 1.12288, 1.12269, 1.12277, 46.0],[1560782400, 1.12278, 1.12299, 1.12276, 1.12297, 43.0],[1560782460, 1.12296, 1.12302, 1.12293, 1.123, 23.0]]}
rep = api.construct_and_send(action="TRADE", actionType="ORDER_TYPE_BUY", symbol="EURUSD", "volume"=0.1, "stoploss"=1.1, "takeprofit"=1.3) ```
print(rep)
``` Buy market order.
Sell limit order. Remember to switch SL/TP depending on BUY/SELL, or you will get `invalid stops` error. ```python
rep = api.construct_and_send(action="TRADE", actionType="ORDER_TYPE_BUY", symbol="EURUSD", "volume"=0.1, "stoploss"=1.1, "takeprofit"=1.3)
- BUY: SL < price < TP print(rep)
- SELL: SL > price > TP ```
``` python Sell limit order. Remember to switch SL/TP depending on BUY/SELL, or you will get `invalid stops` error.
rep = api.construct_and_send(action="TRADE", actionType="ORDER_TYPE_SELL_LIMIT", symbol="EURUSD", "volume"=0.1, "price"=1.2, "stoploss"=1.3, "takeprofit"=1.1)
print(rep) - BUY: SL < price < TP
``` - SELL: SL > price > TP
All pending orders are set to `Good till cancel` by default. If you want to set an expiration date, pass the date in timestamp format to `expiration` param.
```python
``` python rep = api.construct_and_send(action="TRADE", actionType="ORDER_TYPE_SELL_LIMIT", symbol="EURUSD", "volume"=0.1, "price"=1.2, "stoploss"=1.3, "takeprofit"=1.1)
rep = api.construct_and_send(action="TRADE", actionType="ORDER_TYPE_SELL_LIMIT", symbol="EURUSD", "volume"=0.1, "price"=1.2, "expiration"=1560782460) print(rep)
print(rep) ```
```
## Live data and streaming events All pending orders are set to `Good till cancel` by default. If you want to set an expiration date, pass the date in timestamp format to `expiration` param.
Event handler example for `Live socket` and `Data socket`. ```python
rep = api.construct_and_send(action="TRADE", actionType="ORDER_TYPE_SELL_LIMIT", symbol="EURUSD", "volume"=0.1, "price"=1.2, "expiration"=1560782460)
``` python print(rep)
import zmq ```
import threading
## Live data and streaming events
api = MTraderAPI()
Event handler example for `Live socket` and `Data socket`.
def _t_livedata(): ```python
socket = api.live_socket() import zmq
while True: import threading
try:
last_candle = socket.recv_json() api = MTraderAPI()
except zmq.ZMQError:
raise zmq.NotDone("Live data ERROR")
print(last_candle) def _t_livedata():
socket = api.live_socket()
while True:
def _t_streaming_events(): try:
socket = api.streaming_socket() last_candle = socket.recv_json()
while True: except zmq.ZMQError:
try: raise zmq.NotDone("Live data ERROR")
trans = socket.recv_json() print(last_candle)
request, reply = trans.values()
except zmq.ZMQError:
raise zmq.NotDone("Streaming data ERROR") def _t_streaming_events():
print(request) socket = api.streaming_socket()
print(reply) while True:
try:
trans = socket.recv_json()
request, reply = trans.values()
t = threading.Thread(target=_t_livedata, daemon=True) except zmq.ZMQError:
t.start() raise zmq.NotDone("Streaming data ERROR")
print(request)
t = threading.Thread(target=_t_streaming_events, daemon=True) print(reply)
t.start()
while True:
pass t = threading.Thread(target=_t_livedata, daemon=True)
``` t.start()
t = threading.Thread(target=_t_streaming_events, daemon=True)
There are only two variants of `Live socket` data. When everything is ok, the script sends data on candle close: t.start()
``` while True:
{"status":"CONNECTED","data":[1560780120,1.12186,1.12194,1.12186,1.12191,15.00000]} pass
``` ```
If the terminal has lost connection to the market: There are only two variants of `Live socket` data. When everything is ok, the script sends data on candle close:
``` ```
{"status":"DISCONNECTED"} {"status":"CONNECTED","data":[1560780120,1.12186,1.12194,1.12186,1.12191,15.00000]}
``` ```
When the terminal reconnects to the market, it sends the last closed candle again. So you should update your historical data. Make the `action="HISTORY"` request with `fromDate` equal to your last candle timestamp before disconnect. If the terminal has lost connection to the market:
`OnTradeTransaction` function is called when a trade transaction event occurs. `Streaming socket` sends `TRADE_TRANSACTION_REQUEST` data every time it happens. Try to create and modify orders in the MQL5 terminal manually and check the expert logging tab for better understanding. Also see [MQL5 docs](https://www.mql5.com/en/docs/event_handlers/ontradetransaction). ```
{"status":"DISCONNECTED"}
`TRADE_TRANSACTION_REQUEST` request data: ```
``` When the terminal reconnects to the market, it sends the last closed candle again. So you should update your historical data. Make the `action="HISTORY"` request with `fromDate` equal to your last candle timestamp before disconnect.
{
'action': 'TRADE_ACTION_DEAL', `OnTradeTransaction` function is called when a trade transaction event occurs. `Streaming socket` sends `TRADE_TRANSACTION_REQUEST` data every time it happens. Try to create and modify orders in the MQL5 terminal manually and check the expert logging tab for better understanding. Also see [MQL5 docs](https://www.mql5.com/en/docs/event_handlers/ontradetransaction).
'order': 501700843,
'symbol': 'EURUSD', `TRADE_TRANSACTION_REQUEST` request data:
'volume': 0.1,
'price': 1.12181, ```
'stoplimit': 0.0, {
'sl': 1.1, 'action': 'TRADE_ACTION_DEAL',
'tp': 1.13, 'order': 501700843,
'deviation': 10, 'symbol': 'EURUSD',
'type': 'ORDER_TYPE_BUY', 'volume': 0.1,
'type_filling': 'ORDER_FILLING_FOK', 'price': 1.12181,
'type_time': 'ORDER_TIME_GTC', 'stoplimit': 0.0,
'expiration': 0, 'sl': 1.1,
'comment': None, 'tp': 1.13,
'position': 0, 'deviation': 10,
'position_by': 0 'type': 'ORDER_TYPE_BUY',
} 'type_filling': 'ORDER_FILLING_FOK',
``` 'type_time': 'ORDER_TIME_GTC',
`TRADE_TRANSACTION_REQUEST` result data: 'expiration': 0,
'comment': None,
``` 'position': 0,
{ 'position_by': 0
'retcode': 10009, }
'result': 'TRADE_RETCODE_DONE', ```
'deal': 501700843,
'order': 501700843, `TRADE_TRANSACTION_REQUEST` result data:
'volume': 0.1,
'price': 1.12181, ```
'comment': None, {
'request_id': 8, 'retcode': 10009,
'retcode_external': 0 'result': 'TRADE_RETCODE_DONE',
} 'deal': 501700843,
``` 'order': 501700843,
'volume': 0.1,
## Error handling 'price': 1.12181,
First of all, when you send a command via `System socket`, you should always receive back `"OK"` message via `System socket`. It means that your command was received and deserialized. 'comment': None,
'request_id': 8,
All data that come through `Data socket` have an `error` param. This param will have `true` key if somethng goes wrong. Also, there will be `description` and `function` params. They will hold information about error and the name of the function with error. 'retcode_external': 0
}
This information also applies to the trade commannds. See [MQL5 docs](https://www.mql5.com/en/docs/constants/errorswarnings/enum_trade_return_codes) for possible server answers. ```
## License ## Error handling
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
First of all, when you send a command via `System socket`, you should always receive back `"OK"` message via `System socket`. It means that your command was received and deserialized.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See `LICENSE` for more information.
All data that come through `Data socket` have an `error` param. This param will have `true` key if somethng goes wrong. Also, there will be `description` and `function` params. They will hold information about error and the name of the function with error.
This information also applies to the trade commannds. See [MQL5 docs](https://www.mql5.com/en/docs/constants/errorswarnings/enum_trade_return_codes) for possible server answers.
## License
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See `LICENSE` for more information.
+7
View File
@@ -0,0 +1,7 @@
### 11th January 2020
- add support for multiple datastreams in parallel for any combination of symbols and timeframes independently of the timeframe and symbol of the attached chart
- add support for tick data
- add support for direct download as CSV files
- add one automatic retry binding to sockets. When running under Wine in Linux, sockets will be blocked for 60 seconds if closed uncleanly. This can happen if the client is still connected while the EA gets reloaded.
- skip re-initialization on chart timeframe change