├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── images ├── docs_screen.jpg └── intellisense_screen.jpg ├── pymt5adapter ├── __init__.py ├── const.py ├── context.py ├── core.py ├── event.py ├── helpers.py ├── log.py ├── oem.py ├── order.py ├── state.py ├── symbol.py ├── trade.py └── types.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── context.py ├── test_metatrader.py └── test_pymt5adapter.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.logger 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | ### VirtualEnv template 134 | # Virtualenv 135 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 136 | .Python 137 | [Bb]in 138 | [Ii]nclude 139 | [Ll]ib 140 | [Ll]ib64 141 | [Ll]ocal 142 | [Ss]cripts 143 | pyvenv.cfg 144 | .venv 145 | pip-selfcheck.json 146 | 147 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 nicholishen 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | Redistributions of source code must retain the above copyright notice, this list of conditions 9 | and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation and/or other materials 13 | provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 16 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 17 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 18 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 19 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 21 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `pymt5adapter` is a drop-in replacement (wrapper) for the `MetaTrader5` python package by MetaQuotes. 4 | The API functions simply pass through values from the `MetaTrader5` functions, but adds the following functionality 5 | in addition to a more pythonic interface: 6 | 7 | - Typing hinting has been added to all functions and return objects for linting and IDE integration. 8 | Now intellisense will work no matter how nested objects are. ![alt text][intellisence_screen] 9 | - Docstrings have been added to each function 10 | (see official MQL [documentation](https://www.mql5.com/en/docs/integration/python_metatrader5)). 11 | Docs can now be accessed on the fly in the IDE. For example: `Ctrl+Q` in pycharm. ![alt text][docs_screen] 12 | - All params can now be called by keyword. No more positional only args. 13 | - Testing included compliments of `pytest` 14 | - A new context manager has been added to provide a more pythonic interface to the setup and tear-down 15 | of the terminal connection. The use of this context-manager can do the following: 16 | 17 | - Ensures that `mt5.shutdown()` is always called, even if the user code throws an uncaught exception. 18 | - Modifies the global behavior of the API: 19 | - Ensure the terminal has enabled auto-trading. 20 | - Prevents running on real account by default 21 | - Accepts a logger and automatically logs function API calls and order activity. 22 | - Can raise the custom `MT5Error `exception whenever `last_error()[0] != RES_S_OK` (off by default) 23 | 24 | 25 | # Installation 26 | 27 | ``` 28 | pip install -U pymt5adapter 29 | ``` 30 | 31 | The `MetaTrader5` dependency sometimes has issues installing with the `pip` version that automatically gets 32 | packaged inside of the `virualenv` environment. If cannot install `MetaTrader5` then you need to update `pip` 33 | inside of the virtualenv. From the command line within the virual environment use: 34 | 35 | ``` 36 | (inside virtualenv):easy_install -U pip 37 | ``` 38 | 39 | # Import 40 | This should work with any existing `MetaTrader5` script by simply changing the `import` statement from: 41 | ```python 42 | import MetaTrader5 as mt5 43 | ``` 44 | to: 45 | ```python 46 | import pymt5adapter as mt5 47 | ``` 48 | 49 | # Context Manager 50 | 51 | The `connected` function returns a context manager which performs all API setup and tear-down and ensures 52 | that `mt5.shutdown()` is always called. 53 | 54 | ### Hello world 55 | 56 | Using the context manager can be easy as... 57 | 58 | ```python 59 | import pymt5adapter as mt5 60 | 61 | with mt5.connected(): 62 | print(mt5.version()) 63 | 64 | ``` 65 | 66 | and can be customized to modify the entire API 67 | 68 | ```python 69 | import pymt5adapter as mt5 70 | import logging 71 | 72 | logger = mt5.get_logger(path_to_logfile='my_mt5_log.log', loglevel=logging.DEBUG, time_utc=True) 73 | 74 | mt5_connected = mt5.connected( 75 | path=r'C:\Users\user\Desktop\MT5\terminal64.exe', 76 | portable=True, 77 | server='MetaQuotes-Demo', 78 | login=1234567, 79 | password='password1', 80 | timeout=5000, 81 | logger=logger, # default is None 82 | ensure_trade_enabled=True, # default is False 83 | enable_real_trading=False, # default is False 84 | raise_on_errors=True, # default is False 85 | return_as_dict=False, # default is False 86 | return_as_native_python_objects=False, # default is False 87 | ) 88 | with mt5_connected as conn: 89 | try: 90 | num_orders = mt5.history_orders_total("invalid", "arguments") 91 | except mt5.MT5Error as e: 92 | print("We modified the API to throw exceptions for all functions.") 93 | print(f"Error = {e}") 94 | # change error handling behavior at runtime 95 | conn.raise_on_errors = False 96 | try: 97 | num_orders = mt5.history_orders_total("invalid", "arguments") 98 | except mt5.MT5Error: 99 | pass 100 | else: 101 | print('We modified the API to silence Exceptions at runtime') 102 | ``` 103 | 104 | Output: 105 | 106 | ``` 107 | MT5 connection has been initialized. 108 | [history_orders_total(invalid, arguments)][(-2, 'Invalid arguments')][None] 109 | We modified the API to throw exceptions for all functions. 110 | Error = (, "Invalid arguments('invalid', 'arguments'){}") 111 | [history_orders_total(invalid, arguments)][(-2, 'Invalid arguments')][None] 112 | We modified the API to silence Exceptions at runtime 113 | [shutdown()][(1, 'Success')][True] 114 | MT5 connection has been shutdown. 115 | 116 | ``` 117 | 118 | # Exception handling 119 | 120 | The `MetaTrader5` package does not raise exceptions and all errors fail silently 121 | by default. This behavior forces the developer to check each object for 122 | `None` or `empty` state and then call `last_error()` to resolve any possible errors. 123 | One of the primary features of the context manager is extend the ability 124 | to toggle exceptions on/off globally. All raised exceptions are of type `MT5Error`. The 125 | `MT5Error` exception has two properties which are `error_code` and `description`. 126 | 127 | ```python 128 | with mt5.connected(raise_on_errors=True) as conn: 129 | try: 130 | invalid_args = mt5.history_deals_get('sdf', 'asdfa') 131 | print(invalid_args) 132 | except mt5.MT5Error as e: 133 | print(e.error_code, e.description) 134 | if e.error_code is mt5.ERROR_CODE.INVALID_PARAMS: 135 | print('You can use "is" to check identity since we use enums') 136 | conn.raise_on_errors = False 137 | print('Errors will not raise exceptions and default behavior has bene restored at runtime') 138 | invalid_args = mt5.history_deals_get('sdf', 'asdfa') 139 | if not invalid_args: 140 | print(mt5.last_error()) 141 | ``` 142 | OUTPUT 143 | ``` 144 | ERROR_CODE.INVALID_PARAMS Invalid arguments('sdf', 'asdfa'){} 145 | You can use "is" to check identity since we use enums 146 | Errors will not raise exceptions and default behavior has bene restored at runtime 147 | (-2, 'Invalid arguments') 148 | ``` 149 | 150 | # Logging 151 | 152 | Logging functionality has been added to the API and can be utilized by passing any logger that implements the 153 | `logging.Logger` interface to the `logger` param in `connected` context manager. Messages generated from the API 154 | functions are always tab deliminated and include two parts: 155 | 1. A short human readable message. 156 | 2. A JSON dump for easy log parsing and debugging. 157 | ``` 158 | 2020-07-22 18:54:10,399 INFO Terminal Initialize Success {"type": "terminal_connection_state", "state": true} 159 | ``` 160 | For convenience, a preconfigured logger can be retrieved by calling `get_logger` 161 | ```python 162 | logger = mt5.get_logger(path_to_logfile='my_mt5_log.log', loglevel=logging.DEBUG, time_utc=True) 163 | 164 | with mt5.connected(logger=logger): 165 | main() 166 | ``` 167 | Note: The API will only automatically log if a logger is passed into the context manager. The intent was to provide 168 | convenience but not force an opinionated logging schema. 169 | 170 | # Additional features not included in the `MetaTrader5` package 171 | 172 | ### Filter function callbacks 173 | 174 | The following API functions can now except an optional callback for filtering using the named parameter, `function`: 175 | * `symbols_get` 176 | * `orders_get` 177 | * `positions_get` 178 | * `history_deals_get` 179 | * `history_orders_get` 180 | 181 | Example: 182 | 183 | ```python 184 | visible_symbols = mt5.symbols_get(function=lambda s: s.visible) 185 | 186 | def out_deal(deal: mt5.TradeDeal): 187 | return deal.entry == mt5.DEAL_ENTRY_OUT 188 | 189 | out_deals = mt5.history_deals_get(function=out_deal) 190 | ``` 191 | 192 | [intellisence_screen]: https://github.com/nicholishen/pymt5adapter/raw/master/images/intellisense_screen.jpg "intellisence example" 193 | [docs_screen]: https://github.com/nicholishen/pymt5adapter/raw/master/images/docs_screen.jpg "quick docs example" -------------------------------------------------------------------------------- /images/docs_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholishen/pymt5adapter---DEPRECATED/00feca1d2b71e143834cf169571de6c71ad70bf2/images/docs_screen.jpg -------------------------------------------------------------------------------- /images/intellisense_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholishen/pymt5adapter---DEPRECATED/00feca1d2b71e143834cf169571de6c71ad70bf2/images/intellisense_screen.jpg -------------------------------------------------------------------------------- /pymt5adapter/__init__.py: -------------------------------------------------------------------------------- 1 | import MetaTrader5 as _mt5 2 | 3 | from . import types 4 | from .const import * 5 | from .context import connected 6 | from .context import handle_exit 7 | from .core import * 8 | from .helpers import dictify 9 | from .helpers import LogJson 10 | from .helpers import make_native 11 | from .log import get_logger 12 | from .oem import * 13 | 14 | as_dict_all = dictify # alias 15 | 16 | __version__ = { 17 | 'pymt5adapter': '0.4.4', 18 | 'MetaTrader5' : _mt5.__version__, 19 | } 20 | 21 | __author__ = { 22 | 'pymt5adapter': 'nicholishen', 23 | 'MetaTrader5' : _mt5.__author__, 24 | } 25 | 26 | # TODO add logging doc to README 27 | -------------------------------------------------------------------------------- /pymt5adapter/const.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import functools 3 | 4 | 5 | class _MyIntFlag(enum.IntFlag): 6 | @classmethod 7 | def all_flags(cls): 8 | flags = functools.reduce(lambda x, y: x | y, cls) 9 | return flags 10 | 11 | 12 | MIN_TERMINAL_BUILD = 2375 13 | MAX_BARS = 3000 14 | MAX_TICKS = 1_000_000_000 15 | 16 | # timeframes 17 | TIMEFRAME_M1 = 1 18 | TIMEFRAME_M2 = 2 19 | TIMEFRAME_M3 = 3 20 | TIMEFRAME_M4 = 4 21 | TIMEFRAME_M5 = 5 22 | TIMEFRAME_M6 = 6 23 | TIMEFRAME_M10 = 10 24 | TIMEFRAME_M12 = 12 25 | TIMEFRAME_M15 = 15 26 | TIMEFRAME_M20 = 20 27 | TIMEFRAME_M30 = 30 28 | TIMEFRAME_H1 = 1 | 0x4000 29 | TIMEFRAME_H2 = 2 | 0x4000 30 | TIMEFRAME_H4 = 4 | 0x4000 31 | TIMEFRAME_H3 = 3 | 0x4000 32 | TIMEFRAME_H6 = 6 | 0x4000 33 | TIMEFRAME_H8 = 8 | 0x4000 34 | TIMEFRAME_H12 = 12 | 0x4000 35 | TIMEFRAME_D1 = 24 | 0x4000 36 | TIMEFRAME_W1 = 1 | 0x8000 37 | TIMEFRAME_MN1 = 1 | 0xC000 38 | 39 | 40 | class TIMEFRAME(enum.IntEnum): 41 | """timeframes, ENUM_TIMEFRAME 42 | 43 | M1, M2, M3, M4, M5, M6, M10, M12, M15, M20, M30, H1, H2, H3, H4, H6, H8, H12, D1, W1, MN1 44 | """ 45 | M1 = 1 46 | M2 = 2 47 | M3 = 3 48 | M4 = 4 49 | M5 = 5 50 | M6 = 6 51 | M10 = 10 52 | M12 = 12 53 | M15 = 15 54 | M20 = 20 55 | M30 = 30 56 | H1 = 1 | 0x4000 57 | H2 = 2 | 0x4000 58 | H3 = 3 | 0x4000 59 | H4 = 4 | 0x4000 60 | H6 = 6 | 0x4000 61 | H8 = 8 | 0x4000 62 | H12 = 12 | 0x4000 63 | D1 = 24 | 0x4000 64 | W1 = 1 | 0x8000 65 | MN1 = 1 | 0xC000 66 | 67 | 68 | MINUTES_TO_TIMEFRAME = { 69 | 1 : TIMEFRAME.M1, 70 | 2 : TIMEFRAME.M2, 71 | 3 : TIMEFRAME.M3, 72 | 4 : TIMEFRAME.M4, 73 | 5 : TIMEFRAME.M5, 74 | 6 : TIMEFRAME.M6, 75 | 10 : TIMEFRAME.M10, 76 | 12 : TIMEFRAME.M12, 77 | 15 : TIMEFRAME.M15, 78 | 20 : TIMEFRAME.M20, 79 | 30 : TIMEFRAME.M30, 80 | (1 * 60) : TIMEFRAME.H1, 81 | (2 * 60) : TIMEFRAME.H2, 82 | (3 * 60) : TIMEFRAME.H3, 83 | (6 * 60) : TIMEFRAME.H6, 84 | (8 * 60) : TIMEFRAME.H8, 85 | (12 * 60) : TIMEFRAME.H12, 86 | (24 * 60) : TIMEFRAME.D1, 87 | (7 * 24 * 60): TIMEFRAME.W1, 88 | 43200 : TIMEFRAME.MN1, 89 | } 90 | 91 | # tick copy flags 92 | COPY_TICKS_ALL = -1 93 | COPY_TICKS_INFO = 1 94 | COPY_TICKS_TRADE = 2 95 | 96 | 97 | class COPY_TICKS(enum.IntEnum): 98 | """ENUM_COPY_TICKS 99 | 100 | ALL, INFO, TRADE 101 | """ 102 | ALL = -1 103 | INFO = 1 104 | TRADE = 2 105 | 106 | 107 | # tick flags 108 | 109 | TICK_FLAG_BID = 0x02 110 | TICK_FLAG_ASK = 0x04 111 | TICK_FLAG_LAST = 0x08 112 | TICK_FLAG_VOLUME = 0x10 113 | TICK_FLAG_BUY = 0x20 114 | TICK_FLAG_SELL = 0x40 115 | 116 | 117 | class TICK_FLAG(_MyIntFlag): 118 | """ENUM_TICK_FLAG 119 | 120 | BID, ASK, LAST, VOLUME, BUY, SELL 121 | """ 122 | BID = 0x02 123 | ASK = 0x04 124 | LAST = 0x08 125 | VOLUME = 0x10 126 | BUY = 0x20 127 | SELL = 0x40 128 | 129 | 130 | # position type, ENUM_POSITION_TYPE 131 | POSITION_TYPE_BUY = 0 # Buy 132 | POSITION_TYPE_SELL = 1 # Sell 133 | 134 | 135 | class POSITION_TYPE(enum.IntEnum): 136 | """position type, ENUM_POSITION_TYPE 137 | 138 | BUY, SELL 139 | """ 140 | BUY = POSITION_TYPE_BUY 141 | SELL = POSITION_TYPE_SELL 142 | 143 | 144 | # position reason, ENUM_POSITION_REASON 145 | POSITION_REASON_CLIENT = 0 # The position was opened as a result of activation of an order placed from a desktop terminal 146 | POSITION_REASON_MOBILE = 1 # The position was opened as a result of activation of an order placed from a mobile application 147 | POSITION_REASON_WEB = 2 # The position was opened as a result of activation of an order placed from the web platform 148 | POSITION_REASON_EXPERT = 3 # The position was opened as a result of activation of an order placed from an MQL5 program, i.e. an Expert Advisor or a script 149 | 150 | 151 | class POSITION_REASON(enum.IntEnum): 152 | CLIENT = 0 # The position was opened as a result of activation of an order placed from a desktop terminal 153 | MOBILE = 1 # The position was opened as a result of activation of an order placed from a mobile application 154 | WEB = 2 # The position was opened as a result of activation of an order placed from the web platform 155 | EXPERT = 3 # The position was opened as a result of activation of an order placed from an MQL5 program, i.e. an Expert Advisor or a script 156 | 157 | 158 | # order types, ENUM_ORDER_TYPE 159 | ORDER_TYPE_BUY = 0 # Market Buy order 160 | ORDER_TYPE_SELL = 1 # Market Sell order 161 | ORDER_TYPE_BUY_LIMIT = 2 # Buy Limit pending order 162 | ORDER_TYPE_SELL_LIMIT = 3 # Sell Limit pending order 163 | ORDER_TYPE_BUY_STOP = 4 # Buy Stop pending order 164 | ORDER_TYPE_SELL_STOP = 5 # Sell Stop pending order 165 | ORDER_TYPE_BUY_STOP_LIMIT = 6 # Upon reaching the order price, a pending Buy Limit order is placed at the StopLimit price 166 | ORDER_TYPE_SELL_STOP_LIMIT = 7 # Upon reaching the order price, a pending Sell Limit order is placed at the StopLimit price 167 | ORDER_TYPE_CLOSE_BY = 8 # Order to close a position by an opposite one 168 | 169 | 170 | class ORDER_TYPE(enum.IntEnum): 171 | BUY = 0 # Market Buy order 172 | SELL = 1 # Market Sell order 173 | BUY_LIMIT = 2 # Buy Limit pending order 174 | SELL_LIMIT = 3 # Sell Limit pending order 175 | BUY_STOP = 4 # Buy Stop pending order 176 | SELL_STOP = 5 # Sell Stop pending order 177 | BUY_STOP_LIMIT = 6 # Upon reaching the order price, a pending Buy Limit order is placed at the StopLimit price 178 | SELL_STOP_LIMIT = 7 # Upon reaching the order price, a pending Sell Limit order is placed at the StopLimit price 179 | CLOSE_BY = 8 # Order to close a position by an opposite one 180 | 181 | 182 | # order state, ENUM_ORDER_STATE 183 | ORDER_STATE_STARTED = 0 # Order checked, but not yet accepted by broker 184 | ORDER_STATE_PLACED = 1 # Order accepted 185 | ORDER_STATE_CANCELED = 2 # Order canceled by client 186 | ORDER_STATE_PARTIAL = 3 # Order partially executed 187 | ORDER_STATE_FILLED = 4 # Order fully executed 188 | ORDER_STATE_REJECTED = 5 # Order rejected 189 | ORDER_STATE_EXPIRED = 6 # Order expired 190 | ORDER_STATE_REQUEST_ADD = 7 # Order is being registered (placing to the trading system) 191 | ORDER_STATE_REQUEST_MODIFY = 8 # Order is being modified (changing its parameters) 192 | ORDER_STATE_REQUEST_CANCEL = 9 # Order is being deleted (deleting from the trading system) 193 | 194 | 195 | class ORDER_STATE(enum.IntEnum): 196 | STARTED = 0 # Order checked, but not yet accepted by broker 197 | PLACED = 1 # Order accepted 198 | CANCELED = 2 # Order canceled by client 199 | PARTIAL = 3 # Order partially executed 200 | FILLED = 4 # Order fully executed 201 | REJECTED = 5 # Order rejected 202 | EXPIRED = 6 # Order expired 203 | REQUEST_ADD = 7 # Order is being registered (placing to the trading system) 204 | REQUEST_MODIFY = 8 # Order is being modified (changing its parameters) 205 | REQUEST_CANCEL = 9 # Order is being deleted (deleting from the trading system) 206 | 207 | 208 | # ENUM_ORDER_TYPE_FILLING 209 | ORDER_FILLING_FOK = 0 210 | ORDER_FILLING_IOC = 1 211 | ORDER_FILLING_RETURN = 2 212 | 213 | 214 | class ORDER_FILLING(enum.IntEnum): 215 | FOK = 0 216 | IOC = 1 217 | RETURN = 2 218 | 219 | 220 | # ENUM_ORDER_TYPE_TIME 221 | ORDER_TIME_GTC = 0 # Good till cancel order 222 | ORDER_TIME_DAY = 1 # Good till current trade day order 223 | ORDER_TIME_SPECIFIED = 2 # Good till expired order 224 | ORDER_TIME_SPECIFIED_DAY = 3 # The order will be effective till 23:59:59 of the specified day. If this time is outside a trading session, the order expires in the nearest trading time. 225 | 226 | 227 | class ORDER_TIME(enum.IntEnum): 228 | GTC = 0 # Good till cancel order 229 | DAY = 1 # Good till current trade day order 230 | SPECIFIED = 2 # Good till expired order 231 | SPECIFIED_DAY = 3 # The order will be effective till 23:59:59 of the specified day. If this time is outside a trading session, the order expires in the nearest tr 232 | 233 | 234 | # ENUM_ORDER_REASON 235 | ORDER_REASON_CLIENT = 0 # The order was placed from a desktop terminal 236 | ORDER_REASON_MOBILE = 1 # The order was placed from a mobile application 237 | ORDER_REASON_WEB = 2 # The order was placed from a web platform 238 | ORDER_REASON_EXPERT = 3 # The order was placed from an MQL5-program, i.e. by an Expert Advisor or a script 239 | ORDER_REASON_SL = 4 # The order was placed as a result of Stop Loss activation 240 | ORDER_REASON_TP = 5 # The order was placed as a result of Take Profit activation 241 | ORDER_REASON_SO = 6 # The order was placed as a result of the Stop Out event 242 | 243 | 244 | class ORDER_REASON(enum.IntEnum): 245 | CLIENT = 0 # The order was placed from a desktop terminal 246 | MOBILE = 1 # The order was placed from a mobile application 247 | WEB = 2 # The order was placed from a web platform 248 | EXPERT = 3 # The order was placed from an MQL5-program, i.e. by an Expert Advisor or a script 249 | SL = 4 # The order was placed as a result of Stop Loss activation 250 | TP = 5 # The order was placed as a result of Take Profit activation 251 | SO = 6 # The order was placed as a result of the Stop Out event 252 | 253 | 254 | # deal types, ENUM_DEAL_TYPE 255 | 256 | DEAL_TYPE_BUY = 0 # Buy 257 | DEAL_TYPE_SELL = 1 # Sell 258 | DEAL_TYPE_BALANCE = 2 # Balance 259 | DEAL_TYPE_CREDIT = 3 # Credit 260 | DEAL_TYPE_CHARGE = 4 # Additional charge 261 | DEAL_TYPE_CORRECTION = 5 # Correction 262 | DEAL_TYPE_BONUS = 6 # Bonus 263 | DEAL_TYPE_COMMISSION = 7 # Additional commission 264 | DEAL_TYPE_COMMISSION_DAILY = 8 # Daily commission 265 | DEAL_TYPE_COMMISSION_MONTHLY = 9 # Monthly commission 266 | DEAL_TYPE_COMMISSION_AGENT_DAILY = 10 # Daily agent commission 267 | DEAL_TYPE_COMMISSION_AGENT_MONTHLY = 11 # Monthly agent commission 268 | DEAL_TYPE_INTEREST = 12 # Interest rate 269 | DEAL_TYPE_BUY_CANCELED = 13 # Canceled buy deal. 270 | DEAL_TYPE_SELL_CANCELED = 14 # Canceled sell deal. 271 | DEAL_DIVIDEND = 15 # Dividend operations 272 | DEAL_DIVIDEND_FRANKED = 16 # Franked (non-taxable) dividend operations 273 | DEAL_TAX = 17 # Tax charges 274 | 275 | 276 | class DEAL_TYPE(enum.IntEnum): 277 | BUY = 0 # Buy 278 | SELL = 1 # Sell 279 | BALANCE = 2 # Balance 280 | CREDIT = 3 # Credit 281 | CHARGE = 4 # Additional charge 282 | CORRECTION = 5 # Correction 283 | BONUS = 6 # Bonus 284 | COMMISSION = 7 # Additional commission 285 | COMMISSION_DAILY = 8 # Daily commission 286 | COMMISSION_MONTHLY = 9 # Monthly commission 287 | COMMISSION_AGENT_DAILY = 10 # Daily agent commission 288 | COMMISSION_AGENT_MONTHLY = 11 # Monthly agent commission 289 | INTEREST = 12 # Interest rate 290 | BUY_CANCELED = 13 # Canceled buy deal. 291 | SELL_CANCELED = 14 # Canceled sell deal. 292 | DIVIDEND = 15 # Dividend operations 293 | DIVIDEND_FRANKED = 16 # Franked (non-taxable) dividend operations 294 | TAX = 17 # Tax ch 295 | 296 | 297 | # ENUM_DEAL_ENTRY 298 | DEAL_ENTRY_IN = 0 # Entry in 299 | DEAL_ENTRY_OUT = 1 # Entry out 300 | DEAL_ENTRY_INOUT = 2 # Reverse 301 | DEAL_ENTRY_OUT_BY = 3 # Close a position by an opposite one 302 | 303 | 304 | class DEAL_ENTRY(enum.IntEnum): 305 | IN = 0 # Entry in 306 | OUT = 1 # Entry out 307 | INOUT = 2 # Reverse 308 | OUT_BY = 3 # Close a position by an o 309 | 310 | 311 | # ENUM_DEAL_REASON 312 | DEAL_REASON_CLIENT = 0 # The deal was executed as a result of activation of an order placed from a desktop terminal 313 | DEAL_REASON_MOBILE = 1 # The deal was executed as a result of activation of an order placed from a mobile application 314 | DEAL_REASON_WEB = 2 # The deal was executed as a result of activation of an order placed from the web platform 315 | DEAL_REASON_EXPERT = 3 # The deal was executed as a result of activation of an order placed from an MQL5 program, i.e. an Expert Advisor or a script 316 | DEAL_REASON_SL = 4 # The deal was executed as a result of Stop Loss activation 317 | DEAL_REASON_TP = 5 # The deal was executed as a result of Take Profit activation 318 | DEAL_REASON_SO = 6 # The deal was executed as a result of the Stop Out event 319 | DEAL_REASON_ROLLOVER = 7 # The deal was executed due to a rollover 320 | DEAL_REASON_VMARGIN = 8 # The deal was executed after charging the variation margin 321 | DEAL_REASON_SPLIT = 9 # The deal was executed after the split (price reduction) of an instrument, which had an open position during split announcement 322 | 323 | 324 | class DEAL_REASON(enum.IntEnum): 325 | CLIENT = 0 # The deal was executed as a result of activation of an order placed from a desktop terminal 326 | MOBILE = 1 # The deal was executed as a result of activation of an order placed from a mobile application 327 | WEB = 2 # The deal was executed as a result of activation of an order placed from the web platform 328 | EXPERT = 3 # The deal was executed as a result of activation of an order placed from an MQL5 program, i.e. an Expert Advisor or a script 329 | SL = 4 # The deal was executed as a result of Stop Loss activation 330 | TP = 5 # The deal was executed as a result of Take Profit activation 331 | SO = 6 # The deal was executed as a result of the Stop Out event 332 | ROLLOVER = 7 # The deal was executed due to a rollover 333 | VMARGIN = 8 # The deal was executed after charging the variation margin 334 | SPLIT = 9 # The deal was executed after the split (price reduction) of an instrument, which had an open position during split announcement 335 | 336 | 337 | # ENUM_TRADE_REQUEST_ACTIONS, Trade Operation Types 338 | TRADE_ACTION_DEAL = 1 # Place a trade order for an immediate execution with the specified parameters (market order) 339 | TRADE_ACTION_PENDING = 5 # Place a trade order for the execution under specified conditions (pending order) 340 | TRADE_ACTION_SLTP = 6 # Modify Stop Loss and Take Profit values of an opened position 341 | TRADE_ACTION_MODIFY = 7 # Modify the parameters of the order placed previously 342 | TRADE_ACTION_REMOVE = 8 # Delete the pending order placed previously 343 | TRADE_ACTION_CLOSE_BY = 10 # Close a position by an opposite one 344 | 345 | 346 | class TRADE_ACTION(enum.IntEnum): 347 | DEAL = 1 # Place a trade order for an immediate execution with the specified parameters (market order) 348 | PENDING = 5 # Place a trade order for the execution under specified conditions (pending order) 349 | SLTP = 6 # Modify Stop Loss and Take Profit values of an opened position 350 | MODIFY = 7 # Modify the parameters of the order placed previously 351 | REMOVE = 8 # Delete the pending order placed previously 352 | CLOSE_BY = 10 # Close a position by an opposite one 353 | 354 | 355 | # ENUM_SYMBOL_CHART_MODE 356 | SYMBOL_CHART_MODE_BID = 0 357 | SYMBOL_CHART_MODE_LAST = 1 358 | 359 | 360 | class SYMBOL_CHART_MODE(enum.IntEnum): 361 | BID = 0 362 | LAST = 1 363 | 364 | 365 | # ENUM_SYMBOL_CALC_MODE 366 | SYMBOL_CALC_MODE_FOREX = 0 367 | SYMBOL_CALC_MODE_FUTURES = 1 368 | SYMBOL_CALC_MODE_CFD = 2 369 | SYMBOL_CALC_MODE_CFDINDEX = 3 370 | SYMBOL_CALC_MODE_CFDLEVERAGE = 4 371 | SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE = 5 372 | SYMBOL_CALC_MODE_EXCH_STOCKS = 32 373 | SYMBOL_CALC_MODE_EXCH_FUTURES = 33 374 | SYMBOL_CALC_MODE_EXCH_OPTIONS = 34 375 | SYMBOL_CALC_MODE_EXCH_OPTIONS_MARGIN = 36 376 | SYMBOL_CALC_MODE_EXCH_BONDS = 37 377 | SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX = 38 378 | SYMBOL_CALC_MODE_EXCH_BONDS_MOEX = 39 379 | SYMBOL_CALC_MODE_SERV_COLLATERAL = 64 380 | 381 | 382 | class SYMBOL_CALC_MODE(enum.IntEnum): 383 | FOREX = 0 384 | FUTURES = 1 385 | CFD = 2 386 | CFDINDEX = 3 387 | CFDLEVERAGE = 4 388 | FOREX_NO_LEVERAGE = 5 389 | EXCH_STOCKS = 32 390 | EXCH_FUTURES = 33 391 | EXCH_OPTIONS = 34 392 | EXCH_OPTIONS_MARGIN = 36 393 | EXCH_BONDS = 37 394 | EXCH_STOCKS_MOEX = 38 395 | EXCH_BONDS_MOEX = 39 396 | SERV_COLLATERAL = 64 397 | 398 | 399 | # ENUM_SYMBOL_TRADE_MODE 400 | SYMBOL_TRADE_MODE_DISABLED = 0 401 | SYMBOL_TRADE_MODE_LONGONLY = 1 402 | SYMBOL_TRADE_MODE_SHORTONLY = 2 403 | SYMBOL_TRADE_MODE_CLOSEONLY = 3 404 | SYMBOL_TRADE_MODE_FULL = 4 405 | 406 | 407 | class SYMBOL_TRADE_MODE(enum.IntEnum): 408 | DISABLED = 0 409 | LONGONLY = 1 410 | SHORTONLY = 2 411 | CLOSEONLY = 3 412 | FULL = 4 413 | 414 | 415 | # ENUM_SYMBOL_TRADE_EXECUTION 416 | SYMBOL_TRADE_EXECUTION_REQUEST = 0 417 | SYMBOL_TRADE_EXECUTION_INSTANT = 1 418 | SYMBOL_TRADE_EXECUTION_MARKET = 2 419 | SYMBOL_TRADE_EXECUTION_EXCHANGE = 3 420 | 421 | 422 | class SYMBOL_TRADE_EXECUTION(enum.IntEnum): 423 | REQUEST = 0 424 | INSTANT = 1 425 | MARKET = 2 426 | EXCHANGE = 3 427 | 428 | 429 | # ENUM_SYMBOL_SWAP_MODE 430 | SYMBOL_SWAP_MODE_DISABLED = 0 431 | SYMBOL_SWAP_MODE_POINTS = 1 432 | SYMBOL_SWAP_MODE_CURRENCY_SYMBOL = 2 433 | SYMBOL_SWAP_MODE_CURRENCY_MARGIN = 3 434 | SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT = 4 435 | SYMBOL_SWAP_MODE_INTEREST_CURRENT = 5 436 | SYMBOL_SWAP_MODE_INTEREST_OPEN = 6 437 | SYMBOL_SWAP_MODE_REOPEN_CURRENT = 7 438 | SYMBOL_SWAP_MODE_REOPEN_BID = 8 439 | 440 | 441 | class SYMBOL_SWAP_MODE(enum.IntEnum): 442 | DISABLED = 0 443 | POINTS = 1 444 | CURRENCY_SYMBOL = 2 445 | CURRENCY_MARGIN = 3 446 | CURRENCY_DEPOSIT = 4 447 | INTEREST_CURRENT = 5 448 | INTEREST_OPEN = 6 449 | REOPEN_CURRENT = 7 450 | REOPEN_BID = 8 451 | 452 | 453 | # ENUM_DAY_OF_WEEK 454 | DAY_OF_WEEK_SUNDAY = 0 455 | DAY_OF_WEEK_MONDAY = 1 456 | DAY_OF_WEEK_TUESDAY = 2 457 | DAY_OF_WEEK_WEDNESDAY = 3 458 | DAY_OF_WEEK_THURSDAY = 4 459 | DAY_OF_WEEK_FRIDAY = 5 460 | DAY_OF_WEEK_SATURDAY = 6 461 | 462 | 463 | class DAY_OF_WEEK(enum.IntEnum): 464 | SUNDAY = 0 465 | MONDAY = 1 466 | TUESDAY = 2 467 | WEDNESDAY = 3 468 | THURSDAY = 4 469 | FRIDAY = 5 470 | SATURDAY = 6 471 | 472 | 473 | # ENUM_SYMBOL_ORDER_GTC_MODE 474 | SYMBOL_ORDERS_GTC = 0 475 | SYMBOL_ORDERS_DAILY = 1 476 | SYMBOL_ORDERS_DAILY_NO_STOPS = 2 477 | 478 | 479 | class SYMBOL_ORDERS(enum.IntEnum): 480 | GTC = 0 481 | DAILY = 1 482 | DAILY_NO_STOPS = 2 483 | 484 | 485 | # ENUM_SYMBOL_OPTION_RIGHT 486 | SYMBOL_OPTION_RIGHT_CALL = 0 487 | SYMBOL_OPTION_RIGHT_PUT = 1 488 | 489 | 490 | class SYMBOL_OPTION_RIGHT(enum.IntEnum): 491 | CALL = 0 492 | PUT = 1 493 | 494 | 495 | # ENUM_SYMBOL_OPTION_MODE 496 | SYMBOL_OPTION_MODE_EUROPEAN = 0 497 | SYMBOL_OPTION_MODE_AMERICAN = 1 498 | 499 | 500 | class SYMBOL_OPTION_MODE(enum.IntEnum): 501 | EUROPEAN = 0 502 | AMERICAN = 1 503 | 504 | 505 | # ENUM_ACCOUNT_TRADE_MODE 506 | ACCOUNT_TRADE_MODE_DEMO = 0 507 | ACCOUNT_TRADE_MODE_CONTEST = 1 508 | ACCOUNT_TRADE_MODE_REAL = 2 509 | 510 | 511 | class ACCOUNT_TRADE_MODE(enum.IntEnum): 512 | DEMO = 0 513 | CONTEST = 1 514 | REAL = 2 515 | 516 | 517 | # ENUM_ACCOUNT_STOPOUT_MODE 518 | ACCOUNT_STOPOUT_MODE_PERCENT = 0 519 | ACCOUNT_STOPOUT_MODE_MONEY = 1 520 | 521 | 522 | class ACCOUNT_STOPOUT_MODE(enum.IntEnum): 523 | ACCOUNT_STOPOUT_MODE_PERCENT = 0 524 | ACCOUNT_STOPOUT_MODE_MONEY = 1 525 | 526 | 527 | # ENUM_ACCOUNT_MARGIN_MODE 528 | ACCOUNT_MARGIN_MODE_RETAIL_NETTING = 0 529 | ACCOUNT_MARGIN_MODE_EXCHANGE = 1 530 | ACCOUNT_MARGIN_MODE_RETAIL_HEDGING = 2 531 | 532 | 533 | class ACCOUNT_MARGIN_MODE(enum.IntEnum): 534 | ACCOUNT_MARGIN_MODE_RETAIL_NETTING = 0 535 | ACCOUNT_MARGIN_MODE_EXCHANGE = 1 536 | ACCOUNT_MARGIN_MODE_RETAIL_HEDGING = 2 537 | 538 | 539 | # order send/check return codes 540 | TRADE_RETCODE_REQUOTE = 10004 541 | TRADE_RETCODE_REJECT = 10006 542 | TRADE_RETCODE_CANCEL = 10007 543 | TRADE_RETCODE_PLACED = 10008 544 | TRADE_RETCODE_DONE = 10009 545 | TRADE_RETCODE_DONE_PARTIAL = 10010 546 | TRADE_RETCODE_ERROR = 10011 547 | TRADE_RETCODE_TIMEOUT = 10012 548 | TRADE_RETCODE_INVALID = 10013 549 | TRADE_RETCODE_INVALID_VOLUME = 10014 550 | TRADE_RETCODE_INVALID_PRICE = 10015 551 | TRADE_RETCODE_INVALID_STOPS = 10016 552 | TRADE_RETCODE_TRADE_DISABLED = 10017 553 | TRADE_RETCODE_MARKET_CLOSED = 10018 554 | TRADE_RETCODE_NO_MONEY = 10019 555 | TRADE_RETCODE_PRICE_CHANGED = 10020 556 | TRADE_RETCODE_PRICE_OFF = 10021 557 | TRADE_RETCODE_INVALID_EXPIRATION = 10022 558 | TRADE_RETCODE_ORDER_CHANGED = 10023 559 | TRADE_RETCODE_TOO_MANY_REQUESTS = 10024 560 | TRADE_RETCODE_NO_CHANGES = 10025 561 | TRADE_RETCODE_SERVER_DISABLES_AT = 10026 562 | TRADE_RETCODE_CLIENT_DISABLES_AT = 10027 563 | TRADE_RETCODE_LOCKED = 10028 564 | TRADE_RETCODE_FROZEN = 10029 565 | TRADE_RETCODE_INVALID_FILL = 10030 566 | TRADE_RETCODE_CONNECTION = 10031 567 | TRADE_RETCODE_ONLY_REAL = 10032 568 | TRADE_RETCODE_LIMIT_ORDERS = 10033 569 | TRADE_RETCODE_LIMIT_VOLUME = 10034 570 | TRADE_RETCODE_INVALID_ORDER = 10035 571 | TRADE_RETCODE_POSITION_CLOSED = 10036 572 | TRADE_RETCODE_INVALID_CLOSE_VOLUME = 10038 573 | TRADE_RETCODE_CLOSE_ORDER_EXIST = 10039 574 | TRADE_RETCODE_LIMIT_POSITIONS = 10040 575 | TRADE_RETCODE_REJECT_CANCEL = 10041 576 | TRADE_RETCODE_LONG_ONLY = 10042 577 | TRADE_RETCODE_SHORT_ONLY = 10043 578 | TRADE_RETCODE_CLOSE_ONLY = 10044 579 | TRADE_RETCODE_FIFO_CLOSE = 10045 580 | 581 | 582 | class TRADE_RETCODE(enum.IntEnum): 583 | REQUOTE = 10004 584 | REJECT = 10006 585 | CANCEL = 10007 586 | PLACED = 10008 587 | DONE = 10009 588 | DONE_PARTIAL = 10010 589 | ERROR = 10011 590 | TIMEOUT = 10012 591 | INVALID = 10013 592 | INVALID_VOLUME = 10014 593 | INVALID_PRICE = 10015 594 | INVALID_STOPS = 10016 595 | TRADE_DISABLED = 10017 596 | MARKET_CLOSED = 10018 597 | NO_MONEY = 10019 598 | PRICE_CHANGED = 10020 599 | PRICE_OFF = 10021 600 | INVALID_EXPIRATION = 10022 601 | ORDER_CHANGED = 10023 602 | TOO_MANY_REQUESTS = 10024 603 | NO_CHANGES = 10025 604 | SERVER_DISABLES_AT = 10026 605 | CLIENT_DISABLES_AT = 10027 606 | LOCKED = 10028 607 | FROZEN = 10029 608 | INVALID_FILL = 10030 609 | CONNECTION = 10031 610 | ONLY_REAL = 10032 611 | LIMIT_ORDERS = 10033 612 | LIMIT_VOLUME = 10034 613 | INVALID_ORDER = 10035 614 | POSITION_CLOSED = 10036 615 | INVALID_CLOSE_VOLUME = 10038 616 | CLOSE_ORDER_EXIST = 10039 617 | LIMIT_POSITIONS = 10040 618 | REJECT_CANCEL = 10041 619 | LONG_ONLY = 10042 620 | SHORT_ONLY = 10043 621 | CLOSE_ONLY = 10044 622 | FIFO_CLOSE = 10045 623 | 624 | 625 | # functio error codes, last_error() 626 | 627 | 628 | RES_S_OK = 1 # generic success 629 | RES_E_FAIL = -1 # generic fail 630 | RES_E_INVALID_PARAMS = -2 # invalid arguments/parameters 631 | RES_E_NO_MEMORY = -3 # no memory condition 632 | RES_E_NOT_FOUND = -4 # no history 633 | RES_E_INVALID_VERSION = -5 # invalid version 634 | RES_E_AUTH_FAILED = -6 # authorization failed 635 | RES_E_UNSUPPORTED = -7 # unsupported method 636 | RES_E_AUTO_TRADING_DISABLED = -8 # auto-trading disabled 637 | RES_E_INTERNAL_FAIL = -10000 # internal IPC general error 638 | RES_E_INTERNAL_FAIL_SEND = -10001 # internal IPC send failed 639 | RES_E_INTERNAL_FAIL_RECV = -10002 # internal IPC recv failed 640 | RES_E_INTERNAL_FAIL_INIT = -10003 # internal IPC initialization fail 641 | RES_E_INTERNAL_FAIL_CONN = -10004 # internal IPC no ipc 642 | RES_E_INTERNAL_FAIL_TIMEOUT = -10005 # internal timeout 643 | # CUSTOM ERROR CODES ---------------------------------------------------------------------- 644 | RES_X_REAL_ACCOUNT_DISABLED = -200_001 # REAL ACCOUNT TRADING HAS NOT BEEN ENABLED IN THE CONTEXT MANAGER 645 | RES_X_TERMINAL_VERSION_OUTDATED = -200_002 # terminal version is out of date and does not support the current feature. 646 | RES_X_UNKNOWN_ERROR = -200_003 647 | RES_X_INVALID_COMMANDLINE_ARGS = -200_004 648 | 649 | 650 | class ERROR_CODE(enum.IntEnum): 651 | OK = 1 # generic success 652 | FAIL = -1 # generic fail 653 | INVALID_PARAMS = -2 # invalid arguments/parameters 654 | NO_MEMORY = -3 # no memory condition 655 | NOT_FOUND = -4 # no history 656 | INVALID_VERSION = -5 # invalid version 657 | AUTH_FAILED = -6 # authorization failed 658 | UNSUPPORTED = -7 # unsupported method 659 | AUTO_TRADING_DISABLED = -8 # auto-trading disabled 660 | INTERNAL_FAIL = -10000 # internal IPC general error 661 | INTERNAL_FAIL_SEND = -10001 # internal IPC send failed 662 | INTERNAL_FAIL_RECV = -10002 # internal IPC recv failed 663 | INTERNAL_FAIL_INIT = -10003 # internal IPC initialization fail 664 | INTERNAL_FAIL_CONN = -10004 # internal IPC no ipc 665 | INTERNAL_FAIL_TIMEOUT = -10005 # internal timeout 666 | # CUSTOM ERROR CODES ---------------------------------------------------------------------- 667 | 668 | REAL_ACCOUNT_DISABLED = -200_001 # REAL ACCOUNT TRADING HAS NOT BEEN ENABLED IN THE CONTEXT MANAGER 669 | TERMINAL_VERSION_OUTDATED = -200_002 # terminal version is out of date and does not support the current feature. 670 | UNKNOWN_ERROR = -200_003 671 | INVALID_COMMANDLINE_ARGS = -200_004 672 | 673 | 674 | MQL_TRADE_REQUEST_PROPS = dict( 675 | action=int, 676 | magic=int, 677 | order=int, 678 | symbol=str, 679 | volume=float, 680 | price=float, 681 | stoplimit=float, 682 | sl=float, 683 | tp=float, 684 | deviation=int, 685 | type=int, 686 | type_filling=int, 687 | type_time=int, 688 | expiration=int, 689 | comment=str, 690 | position=int, 691 | position_by=int, 692 | ) 693 | 694 | PERIOD_SECONDS = { 695 | TIMEFRAME_M1 : 60, 696 | TIMEFRAME_M2 : 120, 697 | TIMEFRAME_M3 : 180, 698 | TIMEFRAME_M4 : 240, 699 | TIMEFRAME_M5 : 300, 700 | TIMEFRAME_M6 : 360, 701 | TIMEFRAME_M10: 600, 702 | TIMEFRAME_M12: 720, 703 | TIMEFRAME_M15: 900, 704 | TIMEFRAME_M20: 1200, 705 | TIMEFRAME_M30: 1800, 706 | TIMEFRAME_H1 : 3600, 707 | TIMEFRAME_H2 : 7200, 708 | TIMEFRAME_H3 : 10800, 709 | TIMEFRAME_H4 : 14400, 710 | TIMEFRAME_H6 : 21600, 711 | TIMEFRAME_H8 : 28800, 712 | TIMEFRAME_H12: 43200, 713 | TIMEFRAME_D1 : 86400, 714 | TIMEFRAME_W1 : 604800, 715 | TIMEFRAME_MN1: 2592000, 716 | } 717 | -------------------------------------------------------------------------------- /pymt5adapter/context.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import logging 3 | import signal 4 | import sys 5 | import time 6 | from pathlib import Path 7 | 8 | from . import const 9 | from .core import mt5_account_info 10 | from .core import mt5_initialize 11 | from .core import mt5_last_error 12 | from .core import mt5_shutdown 13 | from .core import mt5_terminal_info 14 | from .core import MT5Error 15 | from .helpers import LogJson 16 | from .helpers import reduce_args 17 | from .log import get_logger 18 | from .state import global_state as _state 19 | from .types import * 20 | 21 | Ping = namedtuple('Ping', 'trade_server terminal') 22 | 23 | 24 | class _ContextAwareBase: 25 | __state = _state 26 | 27 | def __new__(cls, *args, **kwargs): 28 | if cls.__state.return_as_dict: 29 | raise MT5Error( 30 | const.ERROR_CODE.UNSUPPORTED, 31 | f"Cannot use {cls.__name__} class when API state set to return all as dict.") 32 | return super().__new__(cls) 33 | 34 | 35 | class connected: 36 | def __init__(self, *, 37 | path: Union[str, Path] = None, 38 | portable: bool = None, 39 | server: str = None, 40 | login: int = None, 41 | password: str = None, 42 | timeout: int = None, 43 | logger: Union[logging.Logger, str, Path] = None, 44 | ensure_trade_enabled: bool = None, 45 | enable_real_trading: bool = None, 46 | raise_on_errors: bool = None, 47 | return_as_dict: bool = False, 48 | return_as_native_python_objects: bool = False, 49 | **kwargs 50 | ): 51 | """Context manager for managing the connection with a MT5 terminal using the python ``with`` statement. 52 | 53 | :param path: Path to terminal. 54 | :param portable: Load terminal in portable mode. 55 | :param server: Server name. 56 | :param login: Account login number. 57 | :param password: Account password. 58 | :param timeout: Connection init timeout. 59 | :param ensure_trade_enabled: Ensure that auto-trading is enabled. Will raise MT5Error when set to True and the terminal auto-trading is disabled. 60 | :param enable_real_trading: Must be explicitly set to True to run on a live account. 61 | :param raise_on_errors: Automatically checks last_error() after each function call and will raise a Mt5Error when the error-code is not RES_S_OK 62 | :param debug_logging: Logs each function call. 63 | :param logger: logging.Logger instance. Setting logger.debugLevel to DEBUG will profile and log function calls 64 | :param return_as_dict: Converts all namedtuple to dictionaries. 65 | :param return_as_native_python_objects: Converts all returns to JSON. Namedtuples become JSON objects and numpy arrays become JSON arrays. 66 | 67 | :param kwargs: 68 | :return: None 69 | 70 | Note: 71 | The param ``enable_real_trading`` must be set to True to work on live accounts. 72 | """ 73 | # DATA VALIDATION 74 | if path: 75 | try: 76 | path = Path(path) 77 | if path.is_dir(): 78 | path = next(path.glob('**/terminal64.exe')) 79 | if not path.exists() or 'terminal64' not in str(path): 80 | raise Exception 81 | path = str(path.absolute()) 82 | except Exception: 83 | if logger: 84 | logger.error(LogJson('Invalid Terminal Path', { 85 | 'type' : 'init_error', 86 | 'exception': { 87 | 'type' : 'MT5Error', 88 | 'message' : 'Invalid path to terminal.', 89 | 'last_error': [const.ERROR_CODE.INVALID_PARAMS.value, 'Invalid path to terminal.'], 90 | } 91 | })) 92 | raise MT5Error(const.ERROR_CODE.INVALID_PARAMS, 'Invalid path to terminal.') 93 | if password: 94 | password = str(password) 95 | 96 | self._init_kwargs = reduce_args(dict( 97 | path=path, portable=portable, server=server, 98 | login=login, password=password, timeout=timeout, 99 | )) 100 | self._ensure_trade_enabled = ensure_trade_enabled 101 | self._enable_real_trading = enable_real_trading 102 | # managing global state 103 | if isinstance(logger, (str, Path)): 104 | self._logger = get_logger(path_to_logfile=logger, loglevel=logging.INFO, time_utc=True) 105 | else: 106 | self._logger = logger 107 | self._raise_on_errors = raise_on_errors 108 | # self._debug_logging = debug_logging 109 | self._terminal_info = None 110 | self._return_as_dict = return_as_dict 111 | self._native_python_objects = return_as_native_python_objects 112 | 113 | def __enter__(self): 114 | self._state_on_enter = _state.get_state() 115 | _state.raise_on_errors = self.raise_on_errors 116 | # _state.debug_logging = self.debug_logging 117 | _state.logger = logger = self._logger 118 | _state.return_as_dict = self.return_as_dict 119 | _state.return_as_native_python_objects = self.native_python_objects 120 | try: 121 | if not mt5_initialize(**self._init_kwargs): 122 | # TODO is this logging in correctly? 123 | last_err = mt5_last_error() 124 | err_code, err_description = last_err 125 | err_code = const.ERROR_CODE(err_code) 126 | if self.logger: 127 | self.logger.critical(LogJson('Initialization Failure', { 128 | 'type' : 'init_error', 129 | 'exception' : { 130 | 'type' : 'MT5Error', 131 | 'message' : err_description, 132 | 'last_error': last_err, 133 | }, 134 | 'init_kwargs': self._init_kwargs, 135 | })) 136 | 137 | raise MT5Error(err_code, err_description) 138 | self._account_info = mt5_account_info() 139 | self._terminal_info = mt5_terminal_info() 140 | if logger: 141 | logger.info(LogJson('Terminal Initialize Success', { 142 | 'type' : 'terminal_connection_state', 143 | 'state': True 144 | })) 145 | logger.info(LogJson('Init TerminalInfo', { 146 | 'type' : 'init_terminal_info', 147 | 'terminal_info': self._terminal_info._asdict() 148 | })) 149 | logger.info(LogJson('Init AccountInfo', { 150 | 'type' : 'init_account_info', 151 | 'account_info': self._account_info._asdict() 152 | })) 153 | if not self._enable_real_trading: 154 | if self._account_info.trade_mode == const.ACCOUNT_TRADE_MODE.REAL: 155 | msg = "Real account trading has not been enabled in the context manager" 156 | if logger: 157 | logger.critical(LogJson('Real Account Trading Disabled', { 158 | 'type' : 'init_error', 159 | 'exception': { 160 | 'type' : 'MT5Error', 161 | 'message' : msg, 162 | 'last_error': [const.ERROR_CODE.REAL_ACCOUNT_DISABLED.value, msg], 163 | } 164 | })) 165 | raise MT5Error(const.ERROR_CODE.REAL_ACCOUNT_DISABLED, msg) 166 | if self._ensure_trade_enabled: 167 | term_info = self.terminal_info 168 | _state.max_bars = term_info.maxbars 169 | if not term_info.trade_allowed: 170 | if logger: 171 | logger.critical(LogJson('Initialization Error', { 172 | 'type' : 'init_error', 173 | 'exception': { 174 | 'type' : 'MT5Error', 175 | 'message' : "Terminal Auto-Trading is disabled.", 176 | 'last_error': [ 177 | const.ERROR_CODE.AUTO_TRADING_DISABLED.value, 178 | "Terminal Auto-Trading is disabled." 179 | ], 180 | } 181 | })) 182 | raise MT5Error(const.ERROR_CODE.AUTO_TRADING_DISABLED, "Terminal Auto-Trading is disabled.") 183 | return self 184 | except: 185 | self.__exit__(*sys.exc_info()) 186 | raise 187 | 188 | def __exit__(self, exc_type, exc_val, exc_tb): 189 | if self.logger and exc_val: 190 | self.logger.critical(LogJson(f'UNCAUGHT EXCEPTION: {exc_val}', { 191 | 'type' : 'exception', 192 | 'last_error': mt5_last_error(), 193 | 'exception' : { 194 | 'type' : exc_type.__name__, 195 | 'message': str(exc_val), 196 | } 197 | })) 198 | mt5_shutdown() 199 | _state.set_defaults(**self._state_on_enter) 200 | if self.logger: 201 | self.logger.info(LogJson('Terminal Shutdown', {'type': 'terminal_connection_state', 'state': False})) 202 | 203 | @property 204 | def logger(self) -> logging.Logger: 205 | return self._logger 206 | 207 | @logger.setter 208 | def logger(self, new_logger): 209 | self._logger = new_logger 210 | _state.logger = new_logger 211 | 212 | @property 213 | def raise_on_errors(self) -> bool: 214 | return self._raise_on_errors 215 | 216 | @raise_on_errors.setter 217 | def raise_on_errors(self, flag: bool): 218 | _state.raise_on_errors = flag 219 | self._raise_on_errors = flag 220 | 221 | @property 222 | def terminal_info(self) -> TerminalInfo: 223 | if self._terminal_info is None: 224 | self._terminal_info = mt5_terminal_info() 225 | return self._terminal_info 226 | 227 | @property 228 | def return_as_dict(self): 229 | return self._return_as_dict 230 | 231 | @return_as_dict.setter 232 | def return_as_dict(self, flag: bool): 233 | _state.return_as_dict = flag 234 | self._return_as_dict = flag 235 | 236 | @property 237 | def native_python_objects(self): 238 | return self._native_python_objects 239 | 240 | @native_python_objects.setter 241 | def native_python_objects(self, flag: bool): 242 | _state.return_as_native_python_objects = flag 243 | self._native_python_objects = flag 244 | 245 | def ping(self) -> Ping: 246 | """Get ping in microseconds for the terminal and trade_server. 247 | Ping attrs = Ping.terminal and Ping.trade_server 248 | 249 | :return: dict with 'server' and 'terminal' ping 250 | """ 251 | timed = time.perf_counter() 252 | self._terminal_info = mt5_terminal_info() 253 | timed = int((time.perf_counter() - timed) * 1000) 254 | res = Ping(trade_server=self.terminal_info.ping_last, 255 | terminal=timed) 256 | return res 257 | 258 | 259 | def _sigterm_handler(signum, frame): 260 | sys.exit(0) 261 | 262 | 263 | _sigterm_handler.__enter_ctx__ = False 264 | 265 | 266 | @contextlib.contextmanager 267 | def handle_exit(callback=None, append=False): 268 | """A context manager which properly handles SIGTERM and SIGINT 269 | (KeyboardInterrupt) signals, registering a function which is 270 | guaranteed to be called after signals are received. 271 | Also, it makes sure to execute previously registered signal 272 | handlers as well (if any). 273 | 274 | >>> app = App() 275 | >>> with handle_exit(app.stop): 276 | ... app.start() 277 | ... 278 | >>> 279 | 280 | If append == False raise RuntimeError if there's already a handler 281 | registered for SIGTERM, otherwise both new and old handlers are 282 | executed in this order. 283 | """ 284 | old_handler = signal.signal(signal.SIGTERM, _sigterm_handler) 285 | if (old_handler != signal.SIG_DFL) and (old_handler != _sigterm_handler): 286 | if not append: 287 | raise RuntimeError("there is already a handler registered for " 288 | "SIGTERM: %r" % old_handler) 289 | 290 | def handler(signum, frame): 291 | try: 292 | _sigterm_handler(signum, frame) 293 | finally: 294 | old_handler(signum, frame) 295 | 296 | signal.signal(signal.SIGTERM, handler) 297 | 298 | if _sigterm_handler.__enter_ctx__: 299 | raise RuntimeError("can't use nested contexts") 300 | _sigterm_handler.__enter_ctx__ = True 301 | 302 | try: 303 | yield 304 | except KeyboardInterrupt: 305 | pass 306 | except SystemExit as err: 307 | # code != 0 refers to an application error (e.g. explicit 308 | # sys.exit('some error') call). 309 | # We don't want that to pass silently. 310 | # Nevertheless, the 'finally' clause below will always 311 | # be executed. 312 | if err.code != 0: 313 | raise 314 | finally: 315 | _sigterm_handler.__enter_ctx__ = False 316 | if callback is not None: 317 | callback() 318 | -------------------------------------------------------------------------------- /pymt5adapter/core.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | import re 4 | import time 5 | from datetime import datetime 6 | 7 | import MetaTrader5 as _mt5 8 | import numpy 9 | 10 | from . import const as _const 11 | from . import helpers as _h 12 | from .state import global_state as _state 13 | from .types import * 14 | 15 | 16 | class MT5Error(Exception): 17 | """The exception class for all MetaTrader5 Errors. 18 | 19 | Example: 20 | >>> try: 21 | >>> raise MT5Error(_const.ERROR_CODE.UNSUPPORTED, "This feature is unsupported.") 22 | >>> except MT5Error as e: 23 | >>> print(e.error_code, e.description) 24 | 25 | 26 | """ 27 | 28 | def __init__(self, error_code: _const.ERROR_CODE, description: str): 29 | """ 30 | 31 | :param error_code: int error code returned my last_error() 32 | :param description: error description 33 | """ 34 | super().__init__(f"{error_code.name}: {description}") 35 | self.errno = self.error_code = error_code 36 | self.strerror = self.description = description 37 | 38 | 39 | def _timed_func(f): 40 | @functools.wraps(f) 41 | def wrapper(*args, **kwargs): 42 | timer = time.perf_counter_ns() 43 | result = f(*args, **kwargs) 44 | timer = time.perf_counter_ns() - timer 45 | wrapper._perf_timer = round(timer / 1e6, 3) 46 | return result 47 | 48 | return wrapper 49 | 50 | 51 | def _context_manager_modified(participation, advanced_features=True): 52 | def decorator(f): 53 | # f.__dispatch = True 54 | @functools.wraps(f) 55 | def pymt5adapter_wrapped_function(*args, **kwargs): 56 | if not participation: 57 | return f(*args, **kwargs) 58 | logger = _state.logger 59 | timed_func = None 60 | if logger and logger.level == logging.DEBUG and f is not order_send: 61 | timed_func = _timed_func(f) 62 | use_func = timed_func or f 63 | try: 64 | result = use_func(*args, **kwargs) 65 | except Exception as e: 66 | if logger: 67 | logger.error(_h.LogJson('EXCEPTION', { 68 | 'type' : 'exception', 69 | 'last_error' : mt5_last_error(), 70 | 'exception' : { 71 | 'type' : type(e).__name__, 72 | 'message': str(e), 73 | }, 74 | 'call_signature': dict(function=use_func.__name__, args=args, kwargs=kwargs) 75 | })) 76 | raise 77 | # make sure we logger before we raise 78 | if advanced_features: 79 | last_err = None 80 | if logger: 81 | if result is None or logger.level == logging.DEBUG: 82 | log = logger.warning if result is None else logger.debug 83 | log_dict = _h.LogJson(short_message_=f'Function Debugging: {use_func.__name__}', 84 | type='function_debugging') 85 | if hasattr(use_func, '_perf_timer'): 86 | log_dict['latency_ms'] = use_func._perf_timer 87 | last_err = mt5_last_error() 88 | log_dict['last_error'] = mt5_last_error() 89 | log_dict['call_signature'] = dict(function=use_func.__name__, args=args, kwargs=kwargs) 90 | # call_sig = f"{f.__name__}({_h.args_to_str(args, kwargs)})" 91 | log(log_dict) 92 | if isinstance(result, OrderSendResult): 93 | response = result._asdict() 94 | request = response.pop('request')._asdict() 95 | request_name = _const.ORDER_TYPE(request['type']).name 96 | response_name = trade_retcode_description(response['retcode']) 97 | request_dict = _h.LogJson(short_message_=f'Order Request: {request_name}', type='order_request') 98 | request_dict['request'] = request 99 | logger.info(request_dict) 100 | response_dict = _h.LogJson(short_message_=f'Order Response: {response_name}', 101 | type='order_response') 102 | if hasattr(use_func, '_perf_timer'): 103 | response_dict['latency_ms'] = use_func._perf_timer 104 | response_dict['response'] = response 105 | logger.info(response_dict) 106 | if result.retcode != _const.TRADE_RETCODE.DONE: 107 | logger.warning(_h.LogJson(f'Order Fail: {response_name}', { 108 | 'type' : 'order_fail', 109 | 'retcode' : result.retcode, 110 | 'description': response_name, 111 | })) 112 | if _state.raise_on_errors: # no need to check last error if we got a result 113 | if isinstance(result, numpy.ndarray): 114 | is_result = True if len(result) > 0 else False 115 | else: 116 | is_result = bool(result) 117 | if not is_result: 118 | error_code, description = last_err or mt5_last_error() 119 | if error_code != _const.ERROR_CODE.OK: 120 | if error_code == _const.ERROR_CODE.INVALID_PARAMS: 121 | description += str(args) + str(kwargs) 122 | raise MT5Error(_const.ERROR_CODE(error_code), description) 123 | if _state.return_as_native_python_objects: 124 | result = _h.make_native(result) 125 | elif _state.return_as_dict: 126 | result = _h.dictify(result) 127 | return result 128 | 129 | if participation: 130 | pymt5adapter_wrapped_function.__dispatch = True 131 | return pymt5adapter_wrapped_function 132 | 133 | return decorator 134 | 135 | 136 | @_context_manager_modified(participation=False) 137 | def parse_args(default_symbol: str = None, 138 | default_timeframe: Union[int, _const.TIMEFRAME] = None, 139 | ) -> Optional[Tuple[str, _const.TIMEFRAME]]: 140 | import sys 141 | try: 142 | symbol = sys.argv[1] 143 | arg2 = sys.argv[2] 144 | timeframe = _const.MINUTES_TO_TIMEFRAME.get(int(arg2)) 145 | if timeframe is None: 146 | timeframe = _const.TIMEFRAME(arg2) 147 | return symbol, timeframe 148 | except Exception: 149 | if default_symbol and default_timeframe: 150 | return default_symbol, _const.TIMEFRAME(default_timeframe) 151 | if _state.raise_on_errors: 152 | raise MT5Error(_const.ERROR_CODE.INVALID_COMMANDLINE_ARGS, f"Invalid command line args {sys.argv}") 153 | else: 154 | return None 155 | 156 | 157 | mt5_account_info = _mt5.account_info 158 | 159 | 160 | @_context_manager_modified(participation=True) 161 | def account_info() -> AccountInfo: 162 | """Get info on the current trading account. The function returns all data that can be obtained using 163 | AccountInfoInteger, AccountInfoDouble and AccountInfoString in one call. 164 | 165 | :return: Return info in the form of a named tuple structure (namedtuple). Return None in case of an error. 166 | The info on the error can be obtained using last_error(). 167 | """ 168 | return mt5_account_info() 169 | 170 | 171 | @_context_manager_modified(participation=True) 172 | def copy_rates(symbol, 173 | timeframe: int, 174 | *, 175 | datetime_from: Union[datetime, int] = None, 176 | datetime_to: Union[datetime, int] = None, 177 | start_pos: int = None, 178 | count: int = None, 179 | ) -> Union[numpy.ndarray, None]: 180 | """Generic function to use keywords to automatically call the correct copy rates function depending on the 181 | keyword args passed in. 182 | 183 | :param symbol: Financial instrument name, for example, "EURUSD". 184 | :param timeframe: Timeframe the bars are requested for. Set by a value from the TIMEFRAME enumeration. 185 | :param datetime_from: Date of opening of the first bar from the requested sample. Set by the 'datetime' 186 | object or as a number of seconds elapsed since 1970.01.01. 187 | :param datetime_to: Date, up to which the bars are requested. Set by the 'datetime' object or as a number 188 | of seconds elapsed since 1970.01.01. Bars with the open time <= date_to are returned. 189 | :param start_pos: Initial index of the bar the data are requested from. The numbering of bars goes from 190 | present to past. Thus, the zero bar means the current one. 191 | :param count: Number of bars to receive. 192 | :return: Returns bars as the numpy array with the named time, open, high, low, close, tick_volume, 193 | spread and real_volume columns. Return None in case of an error. The info on the error can be obtained 194 | using last_error(). 195 | """ 196 | # TODO: test this without args and with count only 197 | symbol = _h.any_symbol(symbol) 198 | try: 199 | if datetime_from is not None: 200 | if count is not None: 201 | return mt5_copy_rates_from(symbol, timeframe, datetime_from, count) 202 | if datetime_to is not None: 203 | return mt5_copy_rates_range(symbol, timeframe, datetime_from, datetime_to) 204 | if all(x is None for x in [datetime_from, datetime_to, start_pos]): 205 | start_pos = 0 206 | count = min((count or _state.max_bars), _state.max_bars - 1) 207 | return mt5_copy_rates_from_pos(symbol, timeframe, start_pos, count) 208 | except SystemError: 209 | return None 210 | 211 | 212 | mt5_copy_rates_from = _mt5.copy_rates_from 213 | 214 | 215 | @_context_manager_modified(participation=True) 216 | def copy_rates_from(symbol, 217 | timeframe: int, 218 | datetime_from: Union[datetime, int], 219 | count: int 220 | ) -> Union[numpy.ndarray, None]: 221 | """Get bars from the MetaTrader 5 terminal starting from the specified date. 222 | 223 | :param symbol: Financial instrument name, for example, "EURUSD". 224 | :param timeframe: Timeframe the bars are requested for. Set by a value from the TIMEFRAME enumeration. 225 | :param datetime_from: Date of opening of the first bar from the requested sample. Set by the 'datetime' 226 | object or as a number of seconds elapsed since 1970.01.01. 227 | :param count: Number of bars to receive. 228 | :return: Returns bars as the numpy array with the named time, open, high, low, close, tick_volume, 229 | spread and real_volume columns. Return None in case of an error. The info on the error can be obtained 230 | using last_error(). 231 | """ 232 | symbol = _h.any_symbol(symbol) 233 | try: 234 | return mt5_copy_rates_from(symbol, timeframe, datetime_from, count) 235 | except SystemError: 236 | return None 237 | 238 | 239 | mt5_copy_rates_from_pos = _mt5.copy_rates_from_pos 240 | 241 | 242 | @_context_manager_modified(participation=True) 243 | def copy_rates_from_pos(symbol, 244 | timeframe: int, 245 | start_pos: int, 246 | count: int 247 | ) -> Union[numpy.ndarray, None]: 248 | """Get bars from the MetaTrader 5 terminal starting from the specified index. 249 | 250 | :param symbol: Financial instrument name, for example, "EURUSD". 251 | :param timeframe: Timeframe the bars are requested for. Set by a value from the TIMEFRAME enumeration. 252 | :param start_pos: Initial index of the bar the data are requested from. The numbering of bars goes from 253 | present to past. Thus, the zero bar means the current one. 254 | :param count: Number of bars to receive. 255 | :return: Returns bars as the numpy array with the named time, open, high, low, close, tick_volume, 256 | spread and real_volume columns. Return None in case of an error. The info on the error can be obtained 257 | using last_error(). 258 | """ 259 | symbol = _h.any_symbol(symbol) 260 | try: 261 | return mt5_copy_rates_from_pos(symbol, timeframe, start_pos, count) 262 | except SystemError: 263 | return None 264 | 265 | 266 | mt5_copy_rates_range = _mt5.copy_rates_range 267 | 268 | 269 | @_context_manager_modified(participation=True) 270 | def copy_rates_range(symbol, 271 | timeframe: int, 272 | datetime_from: Union[datetime, int], 273 | datetime_to: Union[datetime, int] 274 | ) -> Union[numpy.ndarray, None]: 275 | """Get bars from the MetaTrader 5 terminal starting from the specified index. 276 | 277 | :param symbol: Financial instrument name, for example, "EURUSD". 278 | :param timeframe: Timeframe the bars are requested for. Set by a value from the TIMEFRAME enumeration. 279 | :param datetime_from: Date of opening of the first bar from the requested sample. Set by the 'datetime' 280 | object or as a number of seconds elapsed since 1970.01.01. 281 | :param datetime_to: Date, up to which the bars are requested. Set by the 'datetime' object or as a number 282 | of seconds elapsed since 1970.01.01. Bars with the open time <= date_to are returned. 283 | :return: Returns bars as the numpy array with the named time, open, high, low, close, tick_volume, 284 | spread and real_volume columns. Return None in case of an error. The info on the error can be obtained 285 | using last_error(). 286 | """ 287 | symbol = _h.any_symbol(symbol) 288 | try: 289 | return mt5_copy_rates_range(symbol, timeframe, datetime_from, datetime_to) 290 | except SystemError: 291 | return None 292 | 293 | 294 | mt5_copy_ticks_from = _mt5.copy_ticks_from 295 | 296 | 297 | @_context_manager_modified(participation=True) 298 | def copy_ticks_from(symbol, 299 | datetime_from: Union[datetime, int], 300 | count: int, 301 | flags: int, 302 | ) -> Union[numpy.ndarray, None]: 303 | """Get ticks from the MetaTrader 5 terminal starting from the specified date. 304 | 305 | :param symbol: Financial instrument name, for example, "EURUSD". Required unnamed parameter. 306 | :param datetime_from: Date the ticks are requested from. Set by the 'datetime' object or as a number of 307 | seconds elapsed since 1970.01.01. 308 | :param count: Number of ticks to receive. 309 | :param flags: A flag to define the type of the requested ticks. COPY_TICKS_INFO – ticks with Bid and/or 310 | Ask changes, COPY_TICKS_TRADE – ticks with changes in Last and Volume, COPY_TICKS_ALL – all ticks. 311 | Flag values are described in the COPY_TICKS enumeration. 312 | :return: Returns ticks as the numpy array with the named time, bid, ask, last and flags columns. 313 | The 'flags' value can be a combination of flags from the TICK_FLAG enumeration. Return None in case of an error. 314 | The info on the error can be obtained using last_error(). 315 | 316 | Note: 317 | See the CopyTicks function for more information. 318 | 319 | When creating the 'datetime' object, Python uses the local time zone, while MetaTrader 5 stores tick and 320 | bar open time in UTC time zone (without the shift). Therefore, 'datetime' should be created in UTC time 321 | for executing functions that use time. Data received from the MetaTrader 5 terminal has UTC time. 322 | """ 323 | symbol = _h.any_symbol(symbol) 324 | try: 325 | return mt5_copy_ticks_from(symbol, datetime_from, count, flags) 326 | except SystemError: 327 | return None 328 | 329 | 330 | mt5_copy_ticks_range = _mt5.copy_ticks_range 331 | 332 | 333 | @_context_manager_modified(participation=True) 334 | def copy_ticks_range(symbol, 335 | datetime_from: Union[datetime, int], 336 | datetime_to: Union[datetime, int], 337 | flags: int, 338 | ) -> Union[numpy.ndarray, None]: 339 | """Get ticks from the MetaTrader 5 terminal starting from the specified date. 340 | 341 | :param symbol: Financial instrument name, for example, "EURUSD". Required unnamed parameter. 342 | :param datetime_from: Date the ticks are requested from. Set by the 'datetime' object or as a number of 343 | seconds elapsed since 1970.01.01. 344 | :param datetime_to: Date, up to which the ticks are requested. Set by the 'datetime' object or as a number 345 | of seconds elapsed since 1970.01.01. 346 | :param flags: A flag to define the type of the requested ticks. COPY_TICKS_INFO – ticks with Bid and/or 347 | Ask changes, COPY_TICKS_TRADE – ticks with changes in Last and Volume, COPY_TICKS_ALL – all ticks. 348 | Flag values are described in the COPY_TICKS enumeration. 349 | :return: Returns ticks as the numpy array with the named time, bid, ask, last and flags columns. 350 | The 'flags' value can be a combination of flags from the TICK_FLAG enumeration. Return None in case of 351 | an error. The info on the error can be obtained using last_error(). 352 | 353 | Note: 354 | See the CopyTicks function for more information. 355 | 356 | When creating the 'datetime' object, Python uses the local time zone, while MetaTrader 5 stores tick 357 | and bar open time in UTC time zone (without the shift). Therefore, 'datetime' should be created in 358 | UTC time for executing functions that use time. Data received from the MetaTrader 5 terminal has UTC time. 359 | """ 360 | symbol = _h.any_symbol(symbol) 361 | try: 362 | return mt5_copy_ticks_range(symbol, datetime_from, datetime_to, flags) 363 | except SystemError: 364 | return None 365 | 366 | 367 | mt5_history_deals_get = _mt5.history_deals_get 368 | 369 | 370 | @_context_manager_modified(participation=True) 371 | def history_deals_get(datetime_from: datetime = None, 372 | datetime_to: datetime = None, 373 | *, 374 | group: str = None, 375 | ticket: int = None, 376 | position: int = None, 377 | function: Callable = None, 378 | **kwargs 379 | ) -> Tuple[TradeDeal]: 380 | """Get deals from trading history within the specified interval with the ability to filter by ticket or position. 381 | 382 | :param datetime_from: Date the orders are requested from. Set by the 'datetime' object or as a number of seconds 383 | elapsed since 1970.01.01. 384 | :param datetime_to: Date, up to which the orders are requested. Set by the 'datetime' object or as a number of 385 | seconds elapsed since 1970.01.01. 386 | :param group: The filter for arranging a group of necessary symbols. Optional named parameter. If the group is 387 | specified, the function returns only deals meeting a specified criteria for a symbol name. 388 | :param ticket: Ticket of an order (stored in DEAL_ORDER) all deals should be received for. Optional parameter. 389 | :param position: Ticket of a position (stored in DEAL_POSITION_ID) all deals should be received for. Optional 390 | parameter. 391 | :param function: A function that accepts a TradeDeal object and 392 | returns True if that object is to be used else False 393 | :param kwargs: 394 | :return: a tuple of TradeDeal objects 395 | """ 396 | args = locals().copy() 397 | return _h.get_history_type_stuff(mt5_history_deals_get, args) 398 | 399 | 400 | mt5_history_deals_total = _mt5.history_deals_total 401 | 402 | 403 | @_context_manager_modified(participation=True) 404 | def history_deals_total(datetime_from: datetime, datetime_to: datetime, **kwargs) -> int: 405 | """Get the number of ``deals`` in trading history within the specified interval. 406 | 407 | :param datetime_from: Date the orders are requested from. Set by the 'datetime' object or as a number of seconds 408 | elapsed since 1970.01.01. Required unnamed parameter. 409 | :param datetime_to: Date, up to which the orders are requested. Set by the 'datetime' object or as a number of 410 | seconds elapsed since 1970.01.01. Required unnamed parameter. 411 | :param kwargs: 412 | :return: 413 | """ 414 | return mt5_history_deals_total(datetime_from, datetime_to) 415 | 416 | 417 | mt5_history_orders_get = _mt5.history_orders_get 418 | 419 | 420 | @_context_manager_modified(participation=True) 421 | def history_orders_get(datetime_from: datetime = None, 422 | datetime_to: datetime = None, 423 | *, 424 | group: str = None, 425 | ticket: int = None, 426 | position: int = None, 427 | function: Callable = None, 428 | **kwargs 429 | ) -> Tuple[TradeOrder]: 430 | """Get deals from trading history within the specified interval with the ability to filter by ticket or position. 431 | 432 | :param datetime_from: Date the orders are requested from. Set by the 'datetime' object or as a number of 433 | seconds elapsed since 1970.01.01. 434 | :param datetime_to: Date, up to which the orders are requested. Set by the 'datetime' object or as a number of 435 | seconds elapsed since 1970.01.01. 436 | :param group: The filter for arranging a group of necessary symbols. 437 | :param ticket: Ticket of an order (stored in DEAL_ORDER) all deals should be received for. Optional parameter. 438 | :param position: Ticket of a position (stored in DEAL_POSITION_ID) all deals should be received for. 439 | Optional parameter. 440 | :param function: A function that accepts a TradeOrder object and 441 | returns True if that object is to be used else False 442 | :param kwargs: 443 | :return: a tuple of TradeOrder objects 444 | """ 445 | args = locals().copy() 446 | return _h.get_history_type_stuff(mt5_history_orders_get, args) 447 | 448 | 449 | mt5_history_orders_total = _mt5.history_orders_total 450 | 451 | 452 | @_context_manager_modified(participation=True) 453 | def history_orders_total(datetime_from: datetime, datetime_to: datetime, **kwargs) -> int: 454 | """Get the number of orders in trading history within the specified interval. 455 | 456 | :param datetime_from: Date the orders are requested from. Set by the 'datetime' object or as a number of seconds 457 | elapsed since 1970.01.01. Required unnamed parameter. 458 | :param datetime_to: Date, up to which the orders are requested. Set by the 'datetime' object or as a number of 459 | seconds elapsed since 1970.01.01. Required unnamed parameter. 460 | :param kwargs: 461 | :return: 462 | """ 463 | return mt5_history_orders_total(datetime_from, datetime_to) 464 | 465 | 466 | mt5_initialize = _mt5.initialize 467 | 468 | 469 | @_context_manager_modified(participation=True) 470 | def initialize(path: str = None, 471 | *, 472 | login: str = None, 473 | password: str = None, 474 | server: str = None, 475 | portable: bool = False, 476 | timeout: int = None, 477 | **kwargs 478 | ) -> bool: 479 | """Establish a connection with the MetaTrader 5 terminal. Call without parameters. The terminal for connection 480 | is found automatically. 481 | 482 | :param path: Path to the metatrader.exe or metatrader64.exe file. Optional unnamed parameter. It is indicated 483 | first without a parameter name. If the path is not specified, the module attempts to find the executable file on 484 | its own. 485 | :param login: Connection timeout in milliseconds. Optional named parameter. If not specified, the value of 486 | 60 000 (60 seconds) is applied. If the connection is not established within the specified time, the call is 487 | forcibly terminated and the exception is generated. 488 | :param password: Trading account password. Optional named parameter. If the password is not set, the password 489 | saved in the terminal database is applied automatically. 490 | :param server: Trade server name. Optional named parameter. If no server is set, the last used server is applied 491 | automatically. 492 | :param portable: Launch terminal in portable mode 493 | :timeout: Number of milliseconds for timeout 494 | :return: Returns True in case of successful connection to the MetaTrader 5 terminal, otherwise - False. 495 | """ 496 | args = locals().copy() 497 | args = _h.reduce_args(args) 498 | result = mt5_initialize(**args) 499 | return result 500 | 501 | 502 | mt5_last_error = _mt5.last_error 503 | 504 | 505 | @_context_manager_modified(participation=True, advanced_features=False) 506 | def last_error() -> Tuple[int, str]: 507 | """last_error() allows obtaining an error code in case of a failed execution of a MetaTrader 5 library function. 508 | It is similar to GetLastError(). However, it applies its own error codes. 509 | 510 | :return: Return the last error code and description as a tuple. 511 | """ 512 | return mt5_last_error() 513 | 514 | 515 | mt5_login = _mt5.login 516 | 517 | 518 | @_context_manager_modified(participation=True) 519 | def login(login: int, *, 520 | password: str = None, 521 | server: str = None, 522 | timeout: int = None, 523 | **kwargs, 524 | ) -> bool: 525 | """Connect to a trading account using specified parameters. 526 | 527 | :param login: Trading account number. Required unnamed parameter. 528 | :param password: Trading account password. 529 | :param server: Trade server name 530 | :param timeout: Connection timeout in milliseconds. 531 | :param kwargs: 532 | :return: True if success. 533 | """ 534 | args = locals().copy() 535 | args = _h.reduce_args(args) 536 | login = args.pop('login') 537 | return mt5_login(login, **args) 538 | 539 | 540 | mt5_order_calc_margin = _mt5.order_calc_margin 541 | 542 | 543 | @_context_manager_modified(participation=True) 544 | def order_calc_margin(order_type: int, 545 | symbol, 546 | volume: float, 547 | price: float, 548 | ) -> float: 549 | """Return margin in the account currency to perform a specified trading operation. 550 | 551 | :param order_type: Order type taking values from the ORDER_TYPE enumeration 552 | :param symbol: Financial instrument name. 553 | :param volume: Trading operation volume. 554 | :param price: Open price. 555 | :return: Real value if successful, otherwise None. The error info can be obtained using last_error(). 556 | """ 557 | symbol = _h.any_symbol(symbol) 558 | return mt5_order_calc_margin(order_type, symbol, volume, price) 559 | 560 | 561 | mt5_order_calc_profit = _mt5.order_calc_profit 562 | 563 | 564 | @_context_manager_modified(participation=True) 565 | def order_calc_profit(order_type: int, 566 | symbol, 567 | volume: float, 568 | price_open: float, 569 | price_close: float, 570 | ) -> float: 571 | """Return margin in the account currency to perform a specified trading operation. 572 | 573 | :param order_type: Order type taking values from the ORDER_TYPE enumeration 574 | :param symbol: Financial instrument name. 575 | :param volume: Trading operation volume. 576 | :param price_open: Open price. 577 | :param price_close: Close price. 578 | :return: Real value if successful, otherwise None. The error info can be obtained using last_error(). 579 | """ 580 | symbol = _h.any_symbol(symbol) 581 | return mt5_order_calc_profit(order_type, symbol, volume, price_open, price_close) 582 | 583 | 584 | mt5_order_check = _mt5.order_check 585 | 586 | 587 | @_context_manager_modified(participation=True) 588 | def order_check(request: dict = None, 589 | *, 590 | action: int = None, magic: int = None, order: int = None, 591 | symbol=None, volume: float = None, price: float = None, 592 | stoplimit: float = None, sl: float = None, tp: float = None, 593 | deviation: int = None, type: int = None, type_filling: int = None, 594 | type_time: int = None, expiration: datetime = None, 595 | comment: str = None, position: int = None, position_by: int = None, 596 | **kwargs, 597 | ) -> OrderCheckResult: 598 | """Check funds sufficiency for performing a required trading operation. Check result are returned as the 599 | MqlTradeCheckResult structure. 600 | 601 | :param request: Pass the trade request as a preformed dictionary. 602 | :param action: Trade operation type 603 | :param magic: Expert Advisor ID (magic number) 604 | :param order: Order ticket 605 | :param symbol: Trade symbol 606 | :param volume: Requested volume for a deal in lots 607 | :param price: Price 608 | :param stoplimit: Stop-limit level 609 | :param sl: Stop loss level 610 | :param tp: Take profit level 611 | :param deviation: Maximum possible deviation from the requested price 612 | :param type: Order type 613 | :param type_filling: Order execution type 614 | :param type_time: Order expiration type 615 | :param expiration: Order expiration time (for the orders of ORDER_TIME_SPECIFIED type) 616 | :param comment: Order comment 617 | :param position: Position ticket 618 | :param position_by: The ticket of an opposite position 619 | :param kwargs: 620 | :return: OrderSendResult namedtuple 621 | """ 622 | symbol = _h.any_symbol(symbol) 623 | args = locals().copy() 624 | return _h.do_trade_action(mt5_order_check, args) 625 | 626 | 627 | mt5_order_send = _mt5.order_send 628 | 629 | 630 | @_context_manager_modified(participation=True) 631 | @_timed_func 632 | def order_send(request: dict = None, 633 | *, 634 | action: int = None, magic: int = None, order: int = None, 635 | symbol=None, volume: float = None, price: float = None, 636 | stoplimit: float = None, sl: float = None, tp: float = None, 637 | deviation: int = None, type: int = None, type_filling: int = None, 638 | type_time: int = None, expiration: datetime = None, 639 | comment: str = None, position: int = None, position_by: int = None, 640 | **kwargs, 641 | ) -> OrderSendResult: 642 | """Interaction between the client terminal and a trade server for executing the order placing operation 643 | is performed by using trade requests. The trade request is represented by the special predefined structure 644 | of MqlTradeRequest type, which contain all the fields necessary to perform trade deals. The request processing 645 | result is represented by the structure of MqlTradeResult type. 646 | 647 | :param request: Pass the order request as a dictionary. 648 | :param action: Trade operation type 649 | :param magic: Expert Advisor ID (magic number) 650 | :param order: Order ticket 651 | :param symbol: Trade symbol 652 | :param volume: Requested volume for a deal in lots 653 | :param price: Price 654 | :param stoplimit: Stop-limit level 655 | :param sl: Stop loss level 656 | :param tp: Take profit level 657 | :param deviation: Maximum possible deviation from the requested price 658 | :param type: Order type 659 | :param type_filling: Order execution type 660 | :param type_time: Order expiration type 661 | :param expiration: Order expiration time (for the orders of ORDER_TIME_SPECIFIED type) 662 | :param comment: Order comment 663 | :param position: Position ticket 664 | :param position_by: The ticket of an opposite position 665 | :param kwargs: 666 | :return: OrderSendResult namedtuple 667 | """ 668 | args = locals().copy() 669 | return _h.do_trade_action(mt5_order_send, args) 670 | 671 | 672 | mt5_orders_get = _mt5.orders_get 673 | 674 | 675 | @_context_manager_modified(participation=True) 676 | def orders_get(symbol=None, 677 | *, 678 | group: str = None, 679 | ticket: int = None, 680 | function: Callable = None, 681 | **kwargs 682 | ) -> Tuple[TradeOrder]: 683 | """Get active orders with the ability to filter by symbol or ticket. 684 | 685 | :param symbol: Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored. 686 | :param group: The filter for arranging a group of necessary symbols. 687 | :param ticket: Order ticket (ORDER_TICKET). Optional named parameter. 688 | :param function: A function that takes a TradeOrder object as its only arg 689 | and returns truth condition for filtering 690 | :return: tuple of TradeOrder objects 691 | 692 | Note: 693 | The function allows receiving all active orders within one call similar to the OrdersTotal and 694 | OrderSelect tandem. 695 | The group parameter allows sorting out orders by symbols. '*' can be used at the beginning and the 696 | end of a string. 697 | The group parameter may contain several comma separated conditions. A condition can be set as a mask 698 | using '*'. The logical negation symbol '!' can be used for an exclusion. All conditions are applied 699 | sequentially, which means conditions of including to a group should be specified first followed by 700 | an exclusion condition. For example, group="*, !EUR" means that orders for all symbols should be 701 | selected first and the ones containing "EUR" in symbol names should be excluded afterwards. 702 | """ 703 | symbol = _h.any_symbol(symbol) 704 | orders = _h.get_ticket_type_stuff( 705 | mt5_orders_get, 706 | symbol=symbol, 707 | group=group, 708 | ticket=ticket, 709 | function=function) 710 | return orders 711 | 712 | 713 | mt5_orders_total = _mt5.orders_total 714 | 715 | 716 | @_context_manager_modified(participation=True) 717 | def orders_total() -> int: 718 | """Get the number of active orders. 719 | 720 | :return: Integer value. 721 | """ 722 | return mt5_orders_total() 723 | 724 | 725 | @_context_manager_modified(participation=True, advanced_features=False) 726 | def period_seconds(timeframe): 727 | """Get the number of seconds for the respective timeframe 728 | 729 | :param timeframe: 730 | :return: 731 | """ 732 | return _const.PERIOD_SECONDS.get(int(timeframe)) 733 | 734 | 735 | mt5_positions_get = _mt5.positions_get 736 | 737 | 738 | @_context_manager_modified(participation=True) 739 | def positions_get(symbol=None, 740 | *, 741 | group: str = None, 742 | ticket: int = None, 743 | function: Callable = None, 744 | **kwargs 745 | ) -> Tuple[TradePosition]: 746 | """Get open positions with the ability to filter by symbol or ticket. There are three call options. 747 | 748 | :param symbol: 749 | :param group: 750 | :param ticket: 751 | :param function: 752 | :return: 753 | """ 754 | symbol = _h.any_symbol(symbol) 755 | positions = _h.get_ticket_type_stuff( 756 | mt5_positions_get, 757 | symbol=symbol, 758 | group=group, 759 | ticket=ticket, 760 | function=function 761 | ) 762 | return positions 763 | 764 | 765 | mt5_positions_total = _mt5.positions_total 766 | 767 | 768 | @_context_manager_modified(participation=True) 769 | def positions_total() -> int: 770 | """Get the number of open positions. 771 | 772 | :return: Integer value. 773 | """ 774 | return mt5_positions_total() 775 | 776 | 777 | mt5_shutdown = _mt5.shutdown 778 | 779 | 780 | @_context_manager_modified(participation=True) 781 | def shutdown() -> None: 782 | """Close the previously established connection to the MetaTrader 5 terminal. 783 | 784 | :return: None 785 | """ 786 | return mt5_shutdown() 787 | 788 | 789 | mt5_symbol_info = _mt5.symbol_info 790 | 791 | 792 | @_context_manager_modified(participation=True) 793 | def symbol_info(symbol) -> SymbolInfo: 794 | """Get data on the specified financial instrument. 795 | 796 | :param symbol: 797 | :return: Return info in the form of a named tuple structure (namedtuple). Return None in case of an error. 798 | The info on the error can be obtained using last_error(). 799 | """ 800 | symbol = _h.any_symbol(symbol) 801 | return mt5_symbol_info(symbol) 802 | 803 | 804 | mt5_symbol_info_tick = _mt5.symbol_info_tick 805 | 806 | 807 | @_context_manager_modified(participation=True) 808 | def symbol_info_tick(symbol) -> Tick: 809 | """Get the last tick for the specified financial instrument. 810 | 811 | :param symbol: 812 | :return: 813 | """ 814 | symbol = _h.any_symbol(symbol) 815 | return mt5_symbol_info_tick(symbol) 816 | 817 | 818 | # direct access to API function without any added overhead 819 | 820 | 821 | mt5_symbol_select = _mt5.symbol_select 822 | 823 | 824 | @_context_manager_modified(participation=True) 825 | def symbol_select(symbol, enable: bool = True) -> bool: 826 | """Select a symbol in the MarketWatch window or remove a symbol from the window. 827 | 828 | :param symbol: 829 | :param enable: 830 | :return: True if successful, otherwise – False. 831 | """ 832 | symbol = _h.any_symbol(symbol) 833 | return mt5_symbol_select(symbol, enable) 834 | 835 | 836 | mt5_symbols_get = _mt5.symbols_get 837 | 838 | 839 | @_context_manager_modified(participation=True) 840 | def symbols_get(*, 841 | group: str = None, 842 | regex: str = None, 843 | function: Callable = None, 844 | **kwargs 845 | ) -> Union[Tuple[SymbolInfo], None]: 846 | """Get all financial instruments from the MetaTrader 5 terminal. 847 | The group parameter allows sorting out symbols by name. '*' can be used at the beginning and the 848 | end of a string. 849 | The group parameter can be used as a named or an unnamed one. Both options work the same way. 850 | The named option (group="GROUP") makes the code easier to read. 851 | The group parameter may contain several comma separated conditions. A condition can be set as a 852 | mask using '*'. The logical negation symbol '!' can be used for an exclusion. All conditions are applied 853 | sequentially, which means conditions of including to a group should be specified first followed by an 854 | exclusion condition. For example, group="*, !EUR" means that all symbols should be selected first and 855 | the ones containing "EUR" in their names should be excluded afterwards. 856 | Unlike symbol_info(), the symbols_get() function returns data on all requested symbols within a single call. 857 | 858 | :param group: The filter for arranging a group of necessary symbols. Optional parameter. If the group is 859 | specified, the function returns only symbols meeting a specified criteria. 860 | :param regex: Regex pattern for symbol filtering. 861 | :param function: A function that takes a SymbolInfo object as its only arg and returns for filtering 862 | the collection of SymbolInfo results. 863 | :param kwargs: 864 | :return: A tuple of SymbolInfo objects 865 | """ 866 | symbols = mt5_symbols_get(group=group) if group else mt5_symbols_get() 867 | if symbols is None: 868 | if _state.raise_on_errors: 869 | build = version() 870 | if build and build[1] < _const.MIN_TERMINAL_BUILD: 871 | raise MT5Error(_const.ERROR_CODE.TERMINAL_VERSION_OUTDATED, 872 | "The terminal build needs to be updated to support this feature.") 873 | else: 874 | error_code, des = last_error() 875 | if error_code == _const.RES_S_OK: 876 | raise MT5Error(_const.ERROR_CODE.UNKNOWN_ERROR, 877 | "Unknown Error. Is the terminal connected?") 878 | else: 879 | return None 880 | if regex: 881 | if isinstance(regex, str): 882 | regex = re.compile(regex) 883 | symbols = filter(lambda s: regex.match(s.name), symbols) 884 | if function: 885 | symbols = filter(function, symbols) 886 | return tuple(symbols) 887 | 888 | 889 | mt5_symbols_total = _mt5.symbols_total 890 | 891 | 892 | @_context_manager_modified(participation=True) 893 | def symbols_total() -> int: 894 | """Get the number of all financial instruments in the MetaTrader 5 terminal. The function is similar to 895 | SymbolsTotal(). However, it returns the number of all symbols including custom ones and the ones disabled 896 | in MarketWatch. 897 | 898 | :return: 899 | """ 900 | return mt5_symbols_total() 901 | 902 | 903 | mt5_terminal_info = _mt5.terminal_info 904 | 905 | 906 | @_context_manager_modified(participation=True) 907 | def terminal_info() -> TerminalInfo: 908 | """Get the connected MetaTrader 5 client terminal status and settings. The function returns all data that can be 909 | obtained using TerminalInfoInteger, TerminalInfoDouble and TerminalInfoDouble in one call. 910 | 911 | :return: Return info in the form of a named tuple structure (namedtuple). Return None in case of an error. 912 | The info on the error can be obtained using last_error(). 913 | """ 914 | return mt5_terminal_info() 915 | 916 | 917 | @_context_manager_modified(participation=True, advanced_features=False) 918 | def trade_retcode_description(retcode): 919 | try: 920 | return _const.TRADE_RETCODE(int(retcode)).name 921 | except (ValueError, AttributeError): 922 | return "Unknown Trade Retcode" 923 | 924 | 925 | mt5_version = _mt5.version 926 | 927 | 928 | @_context_manager_modified(participation=True) 929 | def version() -> Tuple[int, int, str]: 930 | """Return the MetaTrader 5 terminal version. 931 | 932 | :return: Returns the MetaTrader 5 terminal version, build and release date. Return None in case of an error. 933 | The info on the error can be obtained using last_error(). 934 | """ 935 | return mt5_version() 936 | 937 | 938 | @_context_manager_modified(participation=False, advanced_features=False) 939 | def get_function_dispatch(): 940 | dispatch = dict(sorted((n, f) for n, f in globals().items() if hasattr(f, '__dispatch'))) 941 | return dispatch 942 | -------------------------------------------------------------------------------- /pymt5adapter/event.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import time 3 | from datetime import datetime 4 | from datetime import timedelta 5 | 6 | from . import period_seconds 7 | from .const import COPY_TICKS 8 | from .const import TIMEFRAME 9 | from .core import copy_rates_from_pos 10 | from .core import copy_ticks_range 11 | from .core import symbol_info 12 | from .types import * 13 | 14 | 15 | class EVENT(enum.IntFlag): 16 | TICK_LAST_CHANGE = enum.auto() 17 | NEW_BAR = enum.auto() 18 | 19 | 20 | def iter_event(symbol: Union[str, SymbolInfo], 21 | timeframe: TIMEFRAME, 22 | event_flags: Union[EVENT, int], 23 | sleep: float = 0.001): 24 | if event_flags == 0: 25 | return 26 | symbol = getattr(symbol, 'name', symbol_info(symbol)) 27 | last_tick = None 28 | last_bar = copy_rates_from_pos(symbol.name, timeframe, 0, 1)[0] 29 | psec = timedelta(seconds=period_seconds(timeframe)) 30 | next_bar_time = datetime.fromtimestamp(last_bar['time']) + psec 31 | # save as bool to prevent checking condition on each loop 32 | do_tick_event = EVENT.TICK_LAST_CHANGE in event_flags 33 | do_new_bar_event = EVENT.NEW_BAR in event_flags 34 | 35 | while True: 36 | if do_tick_event: 37 | now = datetime.now() 38 | last_tick_time = now - timedelta(seconds=1) if last_tick is None else datetime.fromtimestamp(last_tick.time) 39 | if last_tick_time > now: 40 | now, last_tick_time = last_tick_time, now 41 | ticks = copy_ticks_range(symbol.name, last_tick_time, now, COPY_TICKS.ALL) 42 | if ticks is not None: 43 | for tick in ticks: 44 | tick = CopyTick(*tick) 45 | if last_tick is None or tick.time_msc > last_tick.time_msc: 46 | yield EVENT.TICK_LAST_CHANGE, tick 47 | last_tick = tick 48 | if do_new_bar_event: 49 | if datetime.now() >= next_bar_time: 50 | bar = copy_rates_from_pos(symbol.name, timeframe, 0, 1)[0] 51 | if (bar_time := bar['time']) != last_bar['time']: 52 | yield EVENT.NEW_BAR, CopyRate(*bar) 53 | last_bar = bar 54 | next_bar_time = datetime.fromtimestamp(bar_time) + psec 55 | time.sleep(sleep) 56 | -------------------------------------------------------------------------------- /pymt5adapter/helpers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from .types import * 4 | 5 | try: 6 | import ujson as json 7 | except ImportError: 8 | import json 9 | 10 | 11 | class LogJson(dict): 12 | def __init__(self, short_message_=None, dictionary_=None, **kwargs): 13 | self.default_short_message = 'JSON ENTRY' 14 | if dictionary_ is None and isinstance(short_message_, dict): 15 | dictionary_, short_message_ = short_message_, None 16 | is_dict = isinstance(dictionary_, dict) 17 | self.short_message = str(short_message_) 18 | if is_dict: 19 | super().__init__(dictionary_) 20 | else: 21 | super().__init__(**kwargs) 22 | 23 | def __str__(self): 24 | type_ = self['type'] 25 | msg = self.short_message or type_ or self.default_short_message 26 | res = f"{msg}\t{json.dumps(self)}" 27 | return res 28 | 29 | 30 | def any_symbol(symbol): 31 | """Pass any symbol object with a name property or string. 32 | 33 | :param symbol: Any symbol. 34 | :return: Symbol as string. 35 | """ 36 | name = getattr(symbol, 'name', symbol) 37 | return name 38 | 39 | 40 | def args_to_str(args: tuple, kwargs: dict): 41 | ar = ', '.join(map(str, args)) 42 | kw = ', '.join(f"{k}={v}" for k, v in kwargs.items()) 43 | return ar + (', ' if ar and kw else '') + kw 44 | 45 | 46 | def __ify(data, apply_methods): 47 | for method in apply_methods: 48 | if hasattr(data, method): # noqa 49 | return __ify(getattr(data, method)(), apply_methods) # noqa 50 | T = type(data) 51 | if T is tuple or T is list: 52 | return T(__ify(i, apply_methods) for i in data) 53 | if T is dict: 54 | return {k: __ify(v, apply_methods) for k, v in data.items()} 55 | return data 56 | 57 | 58 | def dictify(data: Any): 59 | """Convert all nested data returns to native python (pickleable) data structures. Example: List[OrderSendResult] 60 | -> List[dict] 61 | 62 | :param data: Any API returned result from the MetaTrader5 API 63 | :return: 64 | """ 65 | # if hasattr(data, '_asdict'): # noqa 66 | # return dictify(data._asdict()) # noqa 67 | # T = type(data) 68 | # if T is tuple or T is list: 69 | # return T(dictify(i) for i in data) 70 | # if T is dict: 71 | # return {k: dictify(v) for k, v in data.items()} 72 | return __ify(data, ['_asdict']) 73 | 74 | 75 | def make_native(data): 76 | return __ify(data, ['_asdict', 'tolist']) 77 | 78 | 79 | def do_trade_action(func, args): 80 | cleaned = reduce_args(args) 81 | request = cleaned.pop('request', {}) 82 | symbol = cleaned.pop('symbol', None) or request.pop('symbol', None) 83 | cleaned['symbol'] = any_symbol(symbol) 84 | order_request = reduce_combine(request, cleaned) 85 | if 'volume' in order_request and isinstance(order_request['volume'], int): 86 | order_request['volume'] = float(order_request['volume']) 87 | return func(order_request) 88 | 89 | 90 | def get_ticket_type_stuff(func, *, symbol, group, ticket, function): 91 | d = locals().copy() 92 | kw = reduce_args_by_keys(d, ['symbol', 'group', 'ticket']) 93 | items = func(**kw) 94 | # if magic: 95 | # items = filter(lambda p: p.magic == magic, items) 96 | if function: 97 | items = tuple(filter(function, items)) 98 | 99 | if items is None: 100 | items = () 101 | elif type(items) is not tuple: 102 | items = (items,) 103 | return items 104 | 105 | 106 | def get_history_type_stuff(func, args): 107 | args = reduce_args(args) 108 | function = args.pop('function', None) 109 | datetime_from = args.get('datetime_from', None) 110 | datetime_to = args.get('datetime_to', None) 111 | if not args: 112 | datetime_from = datetime(2000, 1, 1) 113 | datetime_to = datetime.now() 114 | if datetime_from is not None and datetime_to is not None: 115 | deals = func(datetime_from, datetime_to, **args) 116 | else: 117 | deals = func(**args) 118 | if function: 119 | deals = tuple(filter(function, deals)) 120 | if deals is None: 121 | deals = () 122 | elif type(deals) is not tuple: 123 | deals = (deals,) 124 | return deals 125 | 126 | 127 | def is_rates_array(array): 128 | try: 129 | rate = array[0] 130 | return type(rate) is tuple and len(rate) == 8 131 | except: 132 | return False 133 | 134 | 135 | def reduce_args(kwargs: dict) -> dict: 136 | return {k: v for k, v in kwargs.items() if v is not None and k != 'kwargs'} 137 | 138 | 139 | def reduce_args_by_keys(d: dict, keys: Iterable) -> dict: 140 | return {k: v for k, v in d.items() if k in keys and v is not None} 141 | 142 | 143 | def reduce_combine(d1: dict, d2: dict): 144 | d1 = reduce_args(d1) 145 | for k, v in d2.items(): 146 | if v is not None: 147 | d1[k] = v 148 | return d1 149 | -------------------------------------------------------------------------------- /pymt5adapter/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from pathlib import Path 4 | 5 | from . import core 6 | from .types import * 7 | 8 | 9 | class _UTCFormatter(logging.Formatter): 10 | converter = time.gmtime 11 | 12 | 13 | _loglevel_map = { 14 | logging.DEBUG : 'DEBUG', 15 | logging.INFO : 'INFO', 16 | logging.WARNING : 'WARNING', 17 | logging.ERROR : 'ERROR', 18 | logging.CRITICAL: 'CRITICAL', 19 | } 20 | 21 | 22 | @core._context_manager_modified(participation=False, advanced_features=False) 23 | def get_logger(path_to_logfile: Union[Path, str], 24 | loglevel: int, 25 | time_utc: bool = True 26 | ) -> logging.Logger: 27 | """Get the default logging.Logger instance for pymy5adapter 28 | 29 | :param path_to_logfile: Path to the logfile destination. This can be a string path or a pathlib.Path object 30 | :param loglevel: This takes the logging loglevel. Same as the parameter from the logging module, eg. logging.INFO 31 | :param time_utc: When True this will output the log lines in UTC time and Local time when False 32 | :return: 33 | """ 34 | try: 35 | cache = get_logger.cache 36 | except AttributeError: 37 | cache = get_logger.cache = {} 38 | 39 | file = Path(path_to_logfile) 40 | name = [ 41 | 'pymt5adapter', 42 | file.name, 43 | _loglevel_map.get(loglevel, 'GENERIC'), 44 | ('time_utc' if time_utc else 'time_local'), 45 | ] 46 | name = '.'.join(name) 47 | if name in cache: 48 | return cache[name] 49 | FORMAT = "%(asctime)s\t%(levelname)s\t%(message)s" 50 | logger = logging.getLogger(name) 51 | logger.setLevel(loglevel) 52 | ch = logging.FileHandler(file) 53 | ch.setLevel(logging.DEBUG) 54 | Formatter = _UTCFormatter if time_utc else logging.Formatter 55 | ch.setFormatter(Formatter(FORMAT)) 56 | logger.addHandler(ch) 57 | cache[name] = logger 58 | return logger 59 | -------------------------------------------------------------------------------- /pymt5adapter/oem.py: -------------------------------------------------------------------------------- 1 | import MetaTrader5 as _mt5 2 | 3 | from . import core 4 | from .types import * 5 | 6 | _mt5_close = _mt5.Close 7 | _mt5_buy = _mt5.Buy 8 | _mt5_sell = _mt5.Sell 9 | _mt5_raw_order = _mt5._RawOrder 10 | 11 | # internal order send 12 | def _RawOrder(order_type: int, 13 | symbol: str, 14 | volume: float, 15 | price: float, 16 | comment: str = None, 17 | ticket: int = None 18 | ) -> OrderSendResult: 19 | return _mt5_raw_order(order_type, symbol, volume, price, comment, ticket) 20 | 21 | 22 | # Close all specific orders 23 | @core._context_manager_modified(participation=True, advanced_features=True) 24 | def Close(symbol: str, *, comment: str = None, ticket: int = None) -> Union[bool, str, None]: 25 | try: 26 | return _mt5_close(symbol, comment=comment, ticket=ticket) 27 | except (TypeError, AttributeError): 28 | return None 29 | 30 | 31 | # Buy order 32 | @core._context_manager_modified(participation=True, advanced_features=True) 33 | def Buy(symbol: str, volume: float, price: float = None, *, 34 | comment: str = None, ticket: int = None) -> Union[OrderSendResult, None]: 35 | try: 36 | return _mt5_buy(symbol, volume, price, comment=comment, ticket=ticket) 37 | except (TypeError, AttributeError): 38 | return None 39 | 40 | 41 | # Sell order 42 | @core._context_manager_modified(participation=True, advanced_features=True) 43 | def Sell(symbol: str, volume: float, price: float = None, *, 44 | comment: str = None, ticket: int = None) -> Union[OrderSendResult, None]: 45 | try: 46 | return _mt5_sell(symbol, volume, price, comment=comment, ticket=ticket) 47 | except (TypeError, AttributeError): 48 | return None 49 | -------------------------------------------------------------------------------- /pymt5adapter/order.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from . import const 4 | from .const import MQL_TRADE_REQUEST_PROPS 5 | from .context import _ContextAwareBase 6 | from .core import order_check 7 | from .core import order_send 8 | from .core import symbol_info_tick 9 | from .helpers import any_symbol 10 | from .helpers import reduce_combine 11 | from .types import * 12 | 13 | 14 | def create_order_request(request: dict = None, *, action: int = None, magic: int = None, 15 | order: int = None, symbol: str = None, volume: float = None, 16 | price: float = None, stoplimit: float = None, sl: float = None, tp: float = None, 17 | deviation: int = None, type: int = None, type_filling: int = None, type_time: int = None, 18 | expiration: int = None, comment: str = None, position: int = None, position_by: int = None, 19 | **kwargs) -> dict: 20 | """Construct a trade request dictionary using the predefined keywords. 21 | 22 | :param request: Pass in an existing request dictionary and the keyword args will update existing or add new. 23 | :return: 24 | """ 25 | return Order(**locals().copy()).request() 26 | 27 | 28 | class Order(_ContextAwareBase): 29 | __slots__ = MQL_TRADE_REQUEST_PROPS.keys() 30 | 31 | @classmethod 32 | def as_buy(cls, **kwargs): 33 | return cls(action=const.TRADE_ACTION.DEAL, type=const.ORDER_TYPE.BUY, **kwargs) 34 | 35 | @classmethod 36 | def as_sell(cls, **kwargs): 37 | return cls(action=const.TRADE_ACTION.DEAL, type=const.ORDER_TYPE.SELL, **kwargs) 38 | 39 | @classmethod 40 | def as_buy_limit(cls, **kwargs): 41 | return cls(action=const.TRADE_ACTION.PENDING, type=const.ORDER_TYPE.BUY_LIMIT, **kwargs) 42 | 43 | @classmethod 44 | def as_sell_limit(cls, **kwargs): 45 | return cls(action=const.TRADE_ACTION.PENDING, type=const.ORDER_TYPE.SELL_LIMIT, **kwargs) 46 | 47 | @classmethod 48 | def as_buy_stop(cls, **kwargs): 49 | return cls(action=const.TRADE_ACTION.PENDING, type=const.ORDER_TYPE.BUY_STOP, **kwargs) 50 | 51 | @classmethod 52 | def as_sell_stop(cls, **kwargs): 53 | return cls(action=const.TRADE_ACTION.PENDING, type=const.ORDER_TYPE.SELL_STOP, **kwargs) 54 | 55 | @classmethod 56 | def as_flatten(cls, position: TradePosition, **kwargs): 57 | res = cls.as_sell() if position.type == const.ORDER_TYPE_BUY else cls.as_buy() 58 | return res(position=position.ticket, symbol=position.symbol, 59 | volume=position.volume, magic=position.magic, **kwargs) 60 | 61 | @classmethod 62 | def as_reverse(cls, position: TradePosition, **kwargs): 63 | res = cls.as_flatten(position) 64 | res.volume *= 2 65 | return res 66 | 67 | @classmethod 68 | def as_adjusted_net_position(cls, position: TradePosition, new_net_position: float, **kwargs): 69 | volume = position.volume 70 | volume = -volume if position.type else volume 71 | new_volume = new_net_position - volume 72 | order_obj = cls.as_sell(**kwargs) if new_volume < 0.0 else cls.as_buy(**kwargs) 73 | order_obj.volume = abs(new_volume) 74 | order_obj.symbol = position.symbol 75 | order_obj.magic = position.magic 76 | order_obj.sl = position.sl 77 | order_obj.tp = position.tp 78 | return order_obj 79 | 80 | @classmethod 81 | def as_modify_sltp(cls, position: Union[TradePosition, int], sl=None, tp=None, **kwargs): 82 | order = cls( 83 | action=const.TRADE_ACTION.SLTP, 84 | position=getattr(position, 'ticket', position), 85 | sl=sl, 86 | tp=tp, 87 | **kwargs 88 | ) 89 | if isinstance(position, TradePosition): 90 | order.symbol = position.symbol 91 | order.sl = sl or position.sl 92 | order.tp = tp or position.tp 93 | return order 94 | 95 | @classmethod 96 | def as_delete_pending(cls, order: Union[TradeOrder, int]): 97 | order_ticket = getattr(order, 'ticket', order) 98 | order_obj = cls( 99 | action=const.TRADE_ACTION.REMOVE, 100 | order=order_ticket, 101 | ) 102 | return order_obj 103 | 104 | def __init__(self, 105 | request: dict = None, *, action: int = None, magic: int = None, 106 | order: int = None, symbol=None, volume: float = None, 107 | price: float = None, stoplimit: float = None, sl: float = None, tp: float = None, 108 | deviation: int = None, type: int = None, type_filling: int = None, type_time: int = None, 109 | expiration: int = None, comment: str = None, position: int = None, position_by: int = None, 110 | **kwargs 111 | ): 112 | super().__init__() 113 | args = locals().copy() 114 | del args['self'] 115 | self.action = self.magic = self.order = self.symbol = self.volume = self.price = None 116 | self.stoplimit = self.sl = self.tp = self.deviation = self.type = self.type_filling = None 117 | self.type_time = self.expiration = self.comment = self.position = self.position_by = None 118 | self.__call__(**args) 119 | 120 | def __call__(self, 121 | request: dict = None, *, action: int = None, magic: int = None, 122 | order: int = None, symbol=None, volume: float = None, 123 | price: float = None, stoplimit: float = None, sl: float = None, tp: float = None, 124 | deviation: int = None, type: int = None, type_filling: int = None, type_time: int = None, 125 | expiration: int = None, comment: str = None, position: int = None, position_by: int = None, 126 | **kwargs 127 | ): 128 | 129 | args = locals().copy() 130 | request = args.pop('request', None) or {} 131 | symbol = args.get('symbol') or request.get('symbol') 132 | args['symbol'] = any_symbol(symbol) 133 | self._set_self_kw(reduce_combine(request, args)) 134 | return self 135 | 136 | def __repr__(self): 137 | name = type(self).__name__ 138 | args = [f'{k}={v}' for k, v in self.request().items()] 139 | sig = f"({', '.join(args)})" 140 | return name + sig 141 | 142 | def _set_self_kw(self, kw: dict): 143 | for k, v in kw.items(): 144 | if v is not None and k in self.__slots__: 145 | setattr(self, k, v) 146 | 147 | def request(self) -> dict: 148 | # TODO check this against a control. Make sure it works when passing request as well as when overriding the rew. 149 | req = {} 150 | for k in self.__slots__: 151 | v = getattr(self, k) 152 | if v is not None: 153 | req[k] = v 154 | # refactor for older python versions 155 | # req = {k: v for k in self.__slots__ if (v := getattr(self, k)) is not None} 156 | return req 157 | 158 | def check(self) -> OrderCheckResult: 159 | # TODO test 160 | return order_check(self.request()) 161 | 162 | def send(self) -> OrderSendResult: 163 | # TODO test 164 | req = self.request() 165 | action = req.get('action') 166 | if action == const.TRADE_ACTION.DEAL: 167 | price = req.get('price') 168 | if price is None: 169 | symbol = req.get('symbol') 170 | if symbol: 171 | t = req.get('type') 172 | tick = symbol_info_tick(symbol) 173 | req['price'] = tick.ask if t == const.ORDER_TYPE.BUY else tick.bid 174 | res = order_send(req) 175 | return res 176 | 177 | def copy(self) -> 'Order': 178 | return copy.deepcopy(self) 179 | -------------------------------------------------------------------------------- /pymt5adapter/state.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class _GlobalState: 5 | """ 6 | Borg shared state class 7 | """ 8 | __shared_state = {} 9 | 10 | def __init__(self): 11 | self.__dict__ = self.__shared_state 12 | if 'max_bars' not in self.__shared_state: 13 | self.set_defaults() 14 | 15 | def set_defaults(self, 16 | raise_on_errors=None, 17 | max_bars=None, 18 | logger=None, 19 | return_as_dict=None, 20 | return_as_native_python_objects=None, 21 | ): 22 | """Initializes the instance variables and provides a method for setting the state with a single call. 23 | 24 | :param debug_logging: 25 | :param raise_on_errors: 26 | :param max_bars: 27 | :param logger: 28 | :param return_as_dict: 29 | :param return_as_native_python_objects: 30 | :return: 31 | """ 32 | self.raise_on_errors = raise_on_errors or False 33 | self.max_bars = max_bars or 100_000 34 | self._logger = logger 35 | self.return_as_dict = return_as_dict or False 36 | self.return_as_native_python_objects = return_as_native_python_objects or False 37 | 38 | def get_state(self): 39 | state = dict( 40 | raise_on_errors=self.raise_on_errors, 41 | max_bars=self.max_bars, 42 | logger=self.logger, 43 | return_as_dict=self.return_as_dict 44 | ) 45 | return state 46 | 47 | @property 48 | def logger(self) -> logging.Logger: 49 | return self._logger 50 | 51 | @logger.setter 52 | def logger(self, new_logger: logging.Logger): 53 | self._logger = new_logger 54 | 55 | 56 | global_state: _GlobalState = _GlobalState() 57 | -------------------------------------------------------------------------------- /pymt5adapter/symbol.py: -------------------------------------------------------------------------------- 1 | from . import const 2 | from .context import _ContextAwareBase 3 | from .core import copy_rates_from_pos 4 | from .core import symbol_info 5 | from .core import symbol_info_tick 6 | from .core import symbol_select 7 | from .types import SymbolInfo 8 | from .types import Union 9 | 10 | 11 | class Symbol(_ContextAwareBase): 12 | def __init__(self, symbol: Union[str, SymbolInfo]): 13 | super().__init__() 14 | self.name = symbol 15 | 16 | @property 17 | def name(self): 18 | return self._name 19 | 20 | @name.setter 21 | def name(self, symbol): 22 | try: 23 | self._name = symbol.name 24 | self._info = symbol 25 | except AttributeError: 26 | self._name = symbol 27 | self._info = None 28 | self._refresh() 29 | 30 | @property 31 | def select(self): 32 | return self._select 33 | 34 | @select.setter 35 | def select(self, enable: bool): 36 | x = symbol_select(self._name, enable) 37 | self._select = enable if x else not enable 38 | 39 | @property 40 | def tick(self): 41 | return self._tick 42 | 43 | @property 44 | def spread(self): 45 | return int(round(self.spread_float / self.trade_tick_size)) 46 | 47 | @property 48 | def bid(self): 49 | return self.tick.bid 50 | 51 | @property 52 | def ask(self): 53 | return self.tick.ask 54 | 55 | @property 56 | def volume(self): 57 | return self.tick.volume 58 | 59 | def _daily(self, key): 60 | rate = copy_rates_from_pos(self.name, const.TIMEFRAME.D1, 0, 1) 61 | try: 62 | return rate[0][key] 63 | except: 64 | return 0 65 | 66 | @property 67 | def day_real_volume(self): 68 | return self._daily('real_volume') 69 | 70 | @property 71 | def day_volume(self): 72 | return self._daily('volume') 73 | 74 | @property 75 | def volume_real(self): 76 | return self.tick.volume_real 77 | 78 | def normalize_price(self, price: float): 79 | ts = self.trade_tick_size 80 | if ts != 0.0: 81 | return round(round(price / ts) * ts, self.digits) 82 | 83 | def tick_calc(self, price: float, num_ticks: int): 84 | """Calculate a new price by number of ticks from the price param. The result is normalized to the 85 | tick-size of the instrument. 86 | 87 | :param price: The price to add or subtract ticks from. 88 | :param num_ticks: number of ticks. If subtracting ticks then this should be a negative number. 89 | :return: A new price adjusted by the number of ticks and normalized to tick-size. 90 | """ 91 | return self.normalize_price(price + num_ticks * self.trade_tick_size) 92 | 93 | def refresh_rates(self): 94 | self._tick = symbol_info_tick(self.name) 95 | return self 96 | 97 | def _refresh(self): 98 | info = self._info or symbol_info(self._name) 99 | self.refresh_rates() 100 | self._select = info.select 101 | # self.spread = info.spread 102 | # self.volume_real = info.volume_real 103 | self.custom = info.custom 104 | self.chart_mode = info.chart_mode 105 | self.visible = info.visible 106 | self.session_deals = info.session_deals 107 | self.session_buy_orders = info.session_buy_orders 108 | self.session_sell_orders = info.session_sell_orders 109 | # self.volume = info.volume 110 | self.volumehigh = info.volumehigh 111 | self.volumelow = info.volumelow 112 | self.digits = info.digits 113 | self.spread_float = info.spread_float 114 | self.ticks_bookdepth = info.ticks_bookdepth 115 | self.trade_calc_mode = info.trade_calc_mode 116 | self.trade_mode = info.trade_mode 117 | self.start_time = info.start_time 118 | self.expiration_time = info.expiration_time 119 | self.trade_stops_level = info.trade_stops_level 120 | self.trade_freeze_level = info.trade_freeze_level 121 | self.trade_exemode = info.trade_exemode 122 | self.swap_mode = info.swap_mode 123 | self.swap_rollover3days = info.swap_rollover3days 124 | self.margin_hedged_use_leg = info.margin_hedged_use_leg 125 | self.expiration_mode = info.expiration_mode 126 | self.filling_mode = info.filling_mode 127 | self.order_mode = info.order_mode 128 | self.order_gtc_mode = info.order_gtc_mode 129 | self.option_mode = info.option_mode 130 | self.option_right = info.option_right 131 | self.bidhigh = info.bidhigh 132 | self.bidlow = info.bidlow 133 | self.askhigh = info.askhigh 134 | self.asklow = info.asklow 135 | self.lasthigh = info.lasthigh 136 | self.lastlow = info.lastlow 137 | self.volumehigh_real = info.volumehigh_real 138 | self.volumelow_real = info.volumelow_real 139 | self.option_strike = info.option_strike 140 | self.point = info.point 141 | self.trade_tick_value = info.trade_tick_value 142 | self.trade_tick_value_profit = info.trade_tick_value_profit 143 | self.trade_tick_value_loss = info.trade_tick_value_loss 144 | self.trade_tick_size = info.trade_tick_size 145 | self.trade_contract_size = info.trade_contract_size 146 | self.trade_accrued_interest = info.trade_accrued_interest 147 | self.trade_face_value = info.trade_face_value 148 | self.trade_liquidity_rate = info.trade_liquidity_rate 149 | self.volume_min = info.volume_min 150 | self.volume_max = info.volume_max 151 | self.volume_step = info.volume_step 152 | self.volume_limit = info.volume_limit 153 | self.swap_long = info.swap_long 154 | self.swap_short = info.swap_short 155 | self.margin_initial = info.margin_initial 156 | self.margin_maintenance = info.margin_maintenance 157 | self.session_volume = info.session_volume 158 | self.session_turnover = info.session_turnover 159 | self.session_interest = info.session_interest 160 | self.session_buy_orders_volume = info.session_buy_orders_volume 161 | self.session_sell_orders_volume = info.session_sell_orders_volume 162 | self.session_open = info.session_open 163 | self.session_close = info.session_close 164 | self.session_aw = info.session_aw 165 | self.session_price_settlement = info.session_price_settlement 166 | self.session_price_limit_min = info.session_price_limit_min 167 | self.session_price_limit_max = info.session_price_limit_max 168 | self.margin_hedged = info.margin_hedged 169 | self.price_change = info.price_change 170 | self.price_volatility = info.price_volatility 171 | self.price_theoretical = info.price_theoretical 172 | self.price_greeks_delta = info.price_greeks_delta 173 | self.price_greeks_theta = info.price_greeks_theta 174 | self.price_greeks_gamma = info.price_greeks_gamma 175 | self.price_greeks_vega = info.price_greeks_vega 176 | self.price_greeks_rho = info.price_greeks_rho 177 | self.price_greeks_omega = info.price_greeks_omega 178 | self.price_sensitivity = info.price_sensitivity 179 | self.basis = info.basis 180 | self.category = info.category 181 | self.currency_base = info.currency_base 182 | self.currency_profit = info.currency_profit 183 | self.currency_margin = info.currency_margin 184 | self.bank = info.bank 185 | self.description = info.description 186 | self.exchange = info.exchange 187 | self.formula = info.formula 188 | self.isin = info.isin 189 | self.page = info.page 190 | self.path = info.path 191 | return self 192 | -------------------------------------------------------------------------------- /pymt5adapter/trade.py: -------------------------------------------------------------------------------- 1 | from . import const 2 | from .context import _ContextAwareBase 3 | from .core import positions_get 4 | from .order import Order 5 | from .symbol import Symbol 6 | from .types import * 7 | 8 | 9 | class Trade(_ContextAwareBase): 10 | 11 | def __init__(self, symbol: Union[str, SymbolInfo], magic=0): 12 | super().__init__() 13 | self._symbol = None 14 | self.symbol = symbol 15 | self.magic = magic 16 | # self._order = Order(symbol=self.symbol, magic=self.magic) 17 | self._position = None 18 | 19 | @property 20 | def symbol(self) -> Union[Symbol, None]: 21 | return self._symbol 22 | 23 | @symbol.setter 24 | def symbol(self, new_symbol): 25 | if isinstance(new_symbol, (str, Symbol, SymbolInfo)): 26 | s = Symbol(new_symbol) 27 | else: 28 | raise TypeError('Wrong assignment type. Must be str, Symbol, or SymbolInfo') 29 | self._symbol = s 30 | 31 | @property 32 | def position(self) -> TradePosition: 33 | if self._position is None: 34 | self.refresh() 35 | return self._position 36 | 37 | def refresh(self): 38 | p = positions_get(symbol=self.symbol.name, function=lambda p: p.magic == self.magic) 39 | self._position = p[0] if p else None 40 | return self 41 | 42 | def _do_market(self, order_constructor, volume: float, comment: str = None, **kwargs) -> OrderSendResult: 43 | price = self.symbol.refresh_rates().tick.ask 44 | order = order_constructor( 45 | symbol=self.symbol, 46 | magic=self.magic, 47 | price=price, 48 | volume=volume, 49 | comment=comment, 50 | **kwargs 51 | ) 52 | result = order.send() 53 | return result 54 | 55 | def buy(self, volume: float, comment: str = None, **kwargs) -> OrderSendResult: 56 | return self._do_market(Order.as_buy, volume, comment, **kwargs) 57 | 58 | def sell(self, volume: float, comment: str = None, **kwargs) -> OrderSendResult: 59 | return self._do_market(Order.as_sell, volume, comment, **kwargs) 60 | 61 | def modify_sltp_by_price(self, sl: int = None, tp: int = None, **kwargs) -> OrderSendResult: 62 | """Modify an existing position by exact prices. 63 | 64 | :param sl: stop loss price 65 | :param tp: take profit price 66 | :param kwargs: kwargs are propagated to the Order class used for the order 67 | :return: Result of order_send 68 | """ 69 | p = kwargs.get('position') or self.refresh().position 70 | if sl: 71 | sl = self.symbol.normalize_price(sl) 72 | if tp: 73 | tp = self.symbol.normalize_price(tp) 74 | order = Order.as_modify_sltp(position=p, sl=sl, tp=tp) 75 | result = order.send() 76 | return result 77 | 78 | def modify_sltp_by_ticks(self, 79 | sl: int = None, 80 | tp: int = None, 81 | price_basis: Union[str, float] = 'open', 82 | **kwargs 83 | ) -> OrderSendResult: 84 | """Modify an existing position by number of ticks. 85 | 86 | :param sl: number of ticks away from the defined price for stoploss 87 | :param tp: number of ticks away from the defined price for take profit 88 | :param price_basis: the price basis for calculation of levels. if a str == 'open' is passed then the stops 89 | will be calculated using the position cost-basis. Otherwise, 'current' will use the current price. 90 | :param kwargs: kwargs are propagated to the Order class used for the order 91 | :return: Result of order_send 92 | """ 93 | position = self.refresh().position 94 | tick_size = self.symbol.trade_tick_size 95 | if isinstance(price_basis, float): 96 | bid = ask = price_basis 97 | elif 'current' in price_basis.lower(): 98 | tick = self.symbol.refresh_rates().tick 99 | bid, ask = tick.bid, tick.ask 100 | else: 101 | bid = ask = position.price_open 102 | if position.type == const.POSITION_TYPE.BUY: 103 | price = bid 104 | stop = -abs(sl) if sl else None 105 | take = abs(tp) if tp else None 106 | else: 107 | price = ask 108 | take = -abs(tp) if tp else None 109 | stop = abs(sl) if sl else None 110 | if stop: 111 | stop = price + stop * tick_size 112 | if take: 113 | take = price + take * tick_size 114 | result = self.modify_sltp_by_price(sl=stop, tp=take, position=position, **kwargs) 115 | return result 116 | -------------------------------------------------------------------------------- /pymt5adapter/types.py: -------------------------------------------------------------------------------- 1 | import MetaTrader5 as _mt5 2 | from collections import namedtuple 3 | 4 | from typing import Callable 5 | from typing import Iterable 6 | from typing import Tuple 7 | from typing import Union 8 | from typing import Any 9 | from typing import Optional 10 | from typing import Type 11 | 12 | # custom namedtuples 13 | CopyRate = namedtuple("CopyRate", "time, open, high, low, close, tick_volume, spread, real_volume") 14 | CopyTick = namedtuple("CopyTick", "time, bid, ask, last, volume, time_msc, flags, volume_real") 15 | # MT5 namedtuple objects for typing 16 | Tick = _mt5.Tick 17 | AccountInfo = _mt5.AccountInfo 18 | SymbolInfo = _mt5.SymbolInfo 19 | TerminalInfo = _mt5.TerminalInfo 20 | OrderCheckResult = _mt5.OrderCheckResult 21 | OrderSendResult = _mt5.OrderSendResult 22 | TradeOrder = _mt5.TradeOrder 23 | TradeDeal = _mt5.TradeDeal 24 | TradeRequest = _mt5.TradeRequest 25 | TradePosition = _mt5.TradePosition 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | MetaTrader5==5.0.31 2 | numpy==1.18.2 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | from setuptools import setup 3 | import pymt5adapter 4 | 5 | with open('README.md') as f: 6 | readme = f.read() 7 | 8 | with open('LICENSE') as f: 9 | license = f.read() 10 | 11 | setup( 12 | name='pymt5adapter', 13 | version=pymt5adapter.__version__.get('pymt5adapter'), 14 | description='A drop in replacement wrapper for the MetaTrader5 package', 15 | long_description_content_type='text/markdown', 16 | long_description=readme, 17 | author='nicholishen', 18 | author_email='nicholishen@tutanota.com', 19 | url='https://github.com/nicholishen/pymt5adapter', 20 | license='MIT', 21 | packages=find_packages(exclude=('tests', 'docs')), 22 | install_requires=['MetaTrader5==5.0.33'], 23 | setup_requires=['wheel'], 24 | python_requires='>=3.6', 25 | ) 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholishen/pymt5adapter---DEPRECATED/00feca1d2b71e143834cf169571de6c71ad70bf2/tests/__init__.py -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 4 | 5 | import pymt5adapter -------------------------------------------------------------------------------- /tests/test_metatrader.py: -------------------------------------------------------------------------------- 1 | # import itertools 2 | # 3 | # import MetaTrader5 as mt5 4 | # import pytest 5 | # 6 | # 7 | # @pytest.fixture(autouse=True) 8 | # def mt5_init_shutdown(): 9 | # try: 10 | # mt5.initialize() 11 | # yield 12 | # finally: 13 | # mt5.shutdown() 14 | # 15 | # 16 | # def make_kwargs_func(func): 17 | # return lambda **kwargs: func(**kwargs) 18 | # 19 | # 20 | # def test_initialize(): 21 | # mt5.shutdown() 22 | # assert isinstance(mt5.initialize(), bool) 23 | # error_code = mt5.last_error()[0] 24 | # assert error_code == mt5.RES_S_OK 25 | # mt5.shutdown() 26 | # assert isinstance(make_kwargs_func(mt5.initialize)(), bool) 27 | # error_code = mt5.last_error()[0] 28 | # assert error_code == mt5.RES_S_OK 29 | # 30 | # 31 | # def test_package_version(): 32 | # import re 33 | # pattern = re.compile(r'^\d+\.\d+\.\d+$') 34 | # assert isinstance(version := mt5.__version__, str) 35 | # assert pattern.match(version) 36 | # 37 | # 38 | # def test_terminal_version(): 39 | # version = mt5.version() 40 | # assert isinstance(version, tuple) 41 | # assert len(version) == 3 42 | # assert isinstance(version[0], int) 43 | # assert isinstance(version[1], int) 44 | # assert isinstance(version[2], str) 45 | # 46 | # 47 | # def test_last_error_res_codes(): 48 | # defined_error_codes = [getattr(mt5, name) for name in dir(mt5) if name.startswith('RES_')] 49 | # for code in defined_error_codes: 50 | # assert isinstance(code, int) 51 | # 52 | # 53 | # def test_last_error(): 54 | # defined_error_codes = [getattr(mt5, name) for name in dir(mt5) if name.startswith('RES_')] 55 | # error = mt5.last_error() 56 | # assert isinstance(error, tuple) 57 | # assert len(error) == 2 58 | # code, description = error 59 | # assert code in defined_error_codes 60 | # assert isinstance(code, int) 61 | # assert isinstance(description, str) 62 | # 63 | # 64 | # def test_account_info(): 65 | # info = mt5.account_info() 66 | # assert isinstance(info, mt5.AccountInfo) 67 | # 68 | # 69 | # def test_terminal_info(): 70 | # info = mt5.terminal_info() 71 | # assert isinstance(info, mt5.TerminalInfo) 72 | # 73 | # 74 | # def test_symbols_total(): 75 | # total = mt5.symbols_total() 76 | # assert total is not None 77 | # assert isinstance(total, int) 78 | # 79 | # 80 | # def test_symbols_get(): 81 | # assert (symbols := mt5.symbols_get()) is not None 82 | # assert make_kwargs_func(mt5.symbols_get)() is not None 83 | # assert isinstance(symbols, tuple) 84 | # assert len(symbols) > 0 85 | # assert isinstance(symbols[0], mt5.SymbolInfo) 86 | # symbols = mt5.symbols_get(group="NON_EXISTENT_GROUP") 87 | # assert symbols is not None 88 | # assert isinstance(symbols, tuple) 89 | # assert len(symbols) == 0 90 | # 91 | # 92 | # def test_symbol_info(): 93 | # info = mt5.symbol_info("EURUSD") 94 | # assert isinstance(info, mt5.SymbolInfo) 95 | # 96 | # 97 | # def test_symbol_info_tick(): 98 | # info = mt5.symbol_info_tick("EURUSD") 99 | # assert isinstance(info, mt5.Tick) 100 | # 101 | # 102 | # def test_symbol_select(): 103 | # selected = mt5.symbol_select("EURUSD") 104 | # assert isinstance(selected, bool) 105 | # 106 | # 107 | # def test_orders_total(): 108 | # total = mt5.orders_total() 109 | # assert isinstance(total, int) 110 | # 111 | # 112 | # def test_orders_get(): 113 | # assert (orders := mt5.orders_get()) is not None 114 | # assert make_kwargs_func(mt5.orders_get)() is not None 115 | # assert isinstance(orders, tuple) 116 | # 117 | # 118 | # def test_positions_total(): 119 | # total = mt5.positions_total() 120 | # assert isinstance(total, int) 121 | # 122 | # 123 | # def test_positions_get(): 124 | # assert (p1 := mt5.positions_get()) is not None 125 | # assert (p2 := make_kwargs_func(mt5.positions_get)()) is not None 126 | # assert len(p1) == len(p2) 127 | # invalid_positions = mt5.positions_get(symbol="FAKE_SYMBOL") 128 | # assert isinstance(invalid_positions, tuple) 129 | # assert len(invalid_positions) == 0 130 | # invalid_positions = mt5.positions_get(ticket=0) 131 | # assert isinstance(invalid_positions, tuple) 132 | # assert len(invalid_positions) == 0 133 | # 134 | # 135 | # def test_history_deals_get(): 136 | # deals = mt5.history_deals_get(ticket=0) 137 | # # assert isinstance(deals, tuple) 138 | # 139 | # 140 | # def test_history_orders_get(): 141 | # orders = mt5.history_orders_get(ticket=0) 142 | # # assert isinstance(orders, tuple) 143 | # 144 | # 145 | # def test_consistency_for_empty_data_returns(): 146 | # def all_same_return_types(results): 147 | # g = itertools.groupby(results, key=lambda v: type(v)) 148 | # return next(g, True) and not next(g, False) 149 | # 150 | # funcs = (mt5.positions_get, mt5.orders_get, mt5.history_orders_get, mt5.history_deals_get,) 151 | # results = [f(ticket=0) for f in funcs] 152 | # assert all_same_return_types(results) 153 | # 154 | # 155 | # def test_copy_ticks_range(): 156 | # from datetime import datetime, timedelta 157 | # time_to = datetime.utcnow() 158 | # time_from = time_to - timedelta(minutes=3) 159 | # ticks = mt5.copy_ticks_range("EPM20", time_from, time_to, mt5.COPY_TICKS_ALL) 160 | # assert mt5.last_error()[0] == mt5.RES_S_OK 161 | # assert len(ticks) > 0 162 | # 163 | # 164 | # # 165 | # # def test_copy_ticks_range(): 166 | # # from datetime import datetime, timedelta 167 | # # import time 168 | # # time_to = datetime.now() 169 | # # time_from = time_to - timedelta(minutes=5) 170 | # # print(mt5.initialize()) 171 | # # for i in range(20): 172 | # # ticks = mt5.copy_ticks_range("USDJPY", time_from, time_to, mt5.COPY_TICKS_ALL) 173 | # # print(mt5.last_error()) 174 | # # print(ticks) 175 | # # if len(ticks) > 0: 176 | # # break 177 | # # time.sleep(1) 178 | # # print(mt5.last_error()[0]) 179 | # # assert mt5.last_error()[0] == mt5.RES_S_OK 180 | # # assert len(ticks) > 0 181 | # # mt5.shutdown() 182 | # 183 | # 184 | # 185 | # 186 | # 187 | # 188 | # if __name__ == "__main__": 189 | # pass 190 | -------------------------------------------------------------------------------- /tests/test_pymt5adapter.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from datetime import datetime 3 | from datetime import timedelta 4 | from datetime import timezone 5 | 6 | import pytest 7 | 8 | from .context import pymt5adapter as mta 9 | from pymt5adapter.state import global_state as state 10 | from pymt5adapter import MT5Error 11 | 12 | 13 | import logging 14 | from pathlib import Path 15 | 16 | LOGPATH = Path.home() / 'Desktop/pytest_mt5.log' 17 | # PYTEST SETUP 18 | @pytest.fixture 19 | def connected(): 20 | logger = mta.get_logger(loglevel=logging.DEBUG, path_to_logfile=LOGPATH) 21 | context = mta.connected( 22 | logger=logger, 23 | ensure_trade_enabled=True, 24 | enable_real_trading=False, 25 | timeout=3000 26 | ) 27 | return context 28 | 29 | 30 | # def test_connected_class(): 31 | # connected = mt5.context.connected_class() 32 | # with connected as conn: 33 | # conn.raise_on_errors = True 34 | # conn.debug_logging = True 35 | # conn. 36 | 37 | 38 | def first_symbol(): 39 | return mta.symbols_get(function=lambda s: s.visible and s.trade_mode == mta.SYMBOL_TRADE_MODE_FULL)[0] 40 | 41 | 42 | def make_kwargs_func(func): 43 | return lambda **kwargs: func(**kwargs) 44 | 45 | 46 | def test_mt5_connection_context(): 47 | from pymt5adapter.context import Ping 48 | state.set_defaults() 49 | assert not state.logger 50 | assert not state.raise_on_errors 51 | connected = mta.connected(timeout=5000) 52 | with connected as conn: 53 | # make sure the conn is setting the global state from properties 54 | assert not state.logger 55 | conn.logger = mta.get_logger(path_to_logfile=LOGPATH, loglevel=logging.DEBUG) 56 | assert state.logger 57 | 58 | assert not state.raise_on_errors 59 | conn.raise_on_errors = True 60 | assert state.raise_on_errors 61 | 62 | with pytest.raises(MT5Error): 63 | x = mta.history_deals_get("sadf", "asdf") 64 | 65 | conn.raise_on_errors = False 66 | try: 67 | x = mta.history_deals_get("sadf", "asdf") 68 | except MT5Error: 69 | pytest.fail("Raised MT5Error when feature was toggled off") 70 | ping = conn.ping() 71 | assert isinstance(ping, Ping) 72 | # pass 73 | 74 | 75 | def test_make_native(): 76 | import json 77 | with mta.connected(return_as_native_python_objects=True) as conn: 78 | rates = mta.copy_rates_from_pos("EURUSD", mta.TIMEFRAME_M1, 0, 1) 79 | account = mta.account_info() 80 | x = dict(rates=rates, account=account) 81 | try: 82 | j = json.dumps(x) 83 | except Exception: 84 | pytest.fail() 85 | 86 | 87 | 88 | def test_return_as_dict_all(connected): 89 | assert mta.as_dict_all is mta.dictify 90 | import pickle 91 | with connected as conn: 92 | conn.return_as_dict = True 93 | symbol_dict = first_symbol() 94 | conn.return_as_dict = False 95 | symbol_tuple = first_symbol() 96 | assert type(symbol_dict) is dict 97 | assert isinstance(symbol_tuple, tuple) 98 | # MetaTrader5 return data is not picklable 99 | with pytest.raises(pickle.PicklingError): 100 | x = pickle.dumps(symbol_tuple) 101 | try: 102 | x = pickle.dumps(symbol_dict) 103 | except pickle.PicklingError: 104 | pytest.fail() 105 | 106 | 107 | def test_terminal_version(connected): 108 | with connected: 109 | version = mta.version() 110 | assert isinstance(version, tuple) 111 | assert len(version) == 3 112 | assert isinstance(version[0], int) 113 | assert isinstance(version[1], int) 114 | assert isinstance(version[2], str) 115 | 116 | 117 | def test_package_version(): 118 | v = mta.__version__ 119 | assert isinstance(v, dict) 120 | assert 'MetaTrader5' in v 121 | assert 'pymt5adapter' in v 122 | assert 'pymt5adapter' in v 123 | 124 | 125 | # CORE TESTING 126 | def test_account_info(connected): 127 | with connected: 128 | info = mta.account_info() 129 | assert isinstance(info, mta.AccountInfo) 130 | 131 | 132 | def test_consistency_for_empty_data_returns(connected): 133 | def all_same_return_types(results): 134 | g = itertools.groupby(results, key=lambda v: type(v)) 135 | return next(g, True) and not next(g, False) 136 | 137 | funcs = (mta.positions_get, mta.orders_get, mta.history_orders_get, mta.history_deals_get,) 138 | with connected: 139 | results = [f(ticket=0) for f in funcs] 140 | assert all_same_return_types(results) 141 | 142 | 143 | def test_copy_rates(connected): 144 | with connected as conn: 145 | rstate = conn.raise_on_errors 146 | conn.raise_on_errors = True 147 | try: 148 | s = first_symbol().name 149 | rates = mta.copy_rates(s, mta.TIMEFRAME_M1, count=10_000) 150 | assert len(rates) > 0 151 | finally: 152 | conn.raise_on_errors = rstate 153 | 154 | 155 | def test_copy_ticks_range(connected): 156 | import time 157 | 158 | with connected: 159 | symbol = first_symbol() 160 | last_bar_time = mta.copy_rates_from_pos(symbol.name, mta.TIMEFRAME.M1, 0, 1)[0]['time'] 161 | time_to = datetime.fromtimestamp(last_bar_time, tz=timezone.utc) 162 | time_from = time_to - timedelta(minutes=3) 163 | 164 | for i in range(20): 165 | ticks = mta.copy_ticks_range(symbol.name, time_from, time_to, mta.COPY_TICKS.ALL) 166 | if len(ticks) > 0: 167 | break 168 | time.sleep(1) 169 | assert mta.last_error()[0] == mta.ERROR_CODE.OK 170 | assert len(ticks) > 0 171 | 172 | 173 | def test_copy_ticks_from(connected): 174 | with connected: 175 | symbol = first_symbol() 176 | last_bar_time = datetime.now() - timedelta( 177 | minutes=1) # mt5.copy_rates_from_pos(symbol.name, mt5.TIMEFRAME.M1, 0, 1)[0]['time'] 178 | print(last_bar_time) 179 | ticks = mta.copy_ticks_from(symbol.name, 180 | datetime_from=last_bar_time, 181 | count=mta.MAX_TICKS, 182 | flags=mta.COPY_TICKS.ALL, ) 183 | assert len(ticks) > 0 184 | 185 | 186 | def test_history_deals_get(connected): 187 | with connected: 188 | deals = mta.history_deals_get(ticket=0) 189 | # assert isinstance(deals, tuple) 190 | 191 | 192 | def test_history_orders_get(connected): 193 | with connected: 194 | orders = mta.history_orders_get(ticket=0) 195 | # assert isinstance(orders, tuple) 196 | 197 | 198 | def test_initialize(connected): 199 | assert isinstance(mta.initialize(), bool) 200 | error_code = mta.last_error()[0] 201 | assert error_code == mta.RES_S_OK 202 | mta.shutdown() 203 | assert isinstance(make_kwargs_func(mta.initialize)(), bool) 204 | error_code = mta.last_error()[0] 205 | assert error_code == mta.RES_S_OK 206 | mta.shutdown() 207 | 208 | 209 | def test_last_error_res_codes(connected): 210 | with connected: 211 | defined_error_codes = [getattr(mta, name) for name in dir(mta) if name.startswith('RES_')] 212 | for code in defined_error_codes: 213 | assert isinstance(code, int) 214 | 215 | 216 | def test_order_class(connected): 217 | from pymt5adapter.order import Order 218 | control = dict(symbol="EURUSD", action=mta.TRADE_ACTION_DEAL, type=mta.ORDER_TYPE_BUY, price=1.2345, 219 | comment="control") 220 | order = Order(control) 221 | assert order.request() == control 222 | order.comment = "control" 223 | assert order.request() == control 224 | order(control, type=mta.ORDER_TYPE_BUY) 225 | assert order.request() == control 226 | 227 | buy_order = Order.as_buy() 228 | assert buy_order.type == mta.ORDER_TYPE_BUY 229 | assert buy_order.action == mta.TRADE_ACTION_DEAL 230 | sell_order = Order.as_sell() 231 | assert sell_order.type == mta.ORDER_TYPE_SELL 232 | assert sell_order.action == mta.TRADE_ACTION_DEAL 233 | 234 | with connected: 235 | symbol = first_symbol() 236 | tick = mta.symbol_info_tick(symbol.name) 237 | result = buy_order(symbol=symbol, volume=1.0).send() 238 | assert isinstance(result, mta.OrderSendResult) 239 | 240 | 241 | def test_last_error(connected): 242 | with connected: 243 | defined_error_codes = [getattr(mta, name) for name in dir(mta) if name.startswith('RES_')] 244 | error = mta.last_error() 245 | assert isinstance(error, tuple) 246 | assert len(error) == 2 247 | code, description = error 248 | assert code in defined_error_codes 249 | assert isinstance(code, int) 250 | assert isinstance(description, str) 251 | 252 | 253 | def test_orders_get(connected): 254 | with connected: 255 | assert (orders := mta.orders_get()) is not None 256 | assert make_kwargs_func(mta.orders_get)() is not None 257 | assert isinstance(orders, tuple) 258 | 259 | 260 | def test_orders_total(connected): 261 | with connected: 262 | total = mta.orders_total() 263 | assert isinstance(total, int) 264 | 265 | 266 | def test_period_seconds(): 267 | assert mta.period_seconds(mta.TIMEFRAME.M1) == 60 268 | assert mta.period_seconds(99999) is None 269 | 270 | 271 | def test_positions_get(connected): 272 | with connected: 273 | assert (p1 := mta.positions_get()) is not None 274 | assert (p2 := make_kwargs_func(mta.positions_get)()) is not None 275 | assert len(p1) == len(p2) 276 | invalid_positions = mta.positions_get(symbol="FAKE_SYMBOL") 277 | assert isinstance(invalid_positions, tuple) 278 | assert len(invalid_positions) == 0 279 | invalid_positions = mta.positions_get(ticket=0) 280 | assert isinstance(invalid_positions, tuple) 281 | assert len(invalid_positions) == 0 282 | 283 | 284 | def test_positions_total(connected): 285 | with connected: 286 | total = mta.positions_total() 287 | assert isinstance(total, int) 288 | 289 | 290 | def test_symbol_info(connected): 291 | with connected: 292 | info = mta.symbol_info("EURUSD") 293 | assert isinstance(info, mta.SymbolInfo) 294 | 295 | 296 | def test_symbol_info_tick(connected): 297 | with connected: 298 | info = mta.symbol_info_tick("EURUSD") 299 | print(type(info)) 300 | assert isinstance(info, mta.Tick) 301 | 302 | 303 | def test_symbol_select(connected): 304 | with connected: 305 | selected = mta.symbol_select("EURUSD") 306 | assert isinstance(selected, bool) 307 | 308 | 309 | def test_symbols_get(connected): 310 | with connected: 311 | assert (symbols := mta.symbols_get()) is not None 312 | assert make_kwargs_func(mta.symbols_get)() is not None 313 | assert isinstance(symbols, tuple) 314 | assert len(symbols) > 0 315 | assert isinstance(symbols[0], mta.SymbolInfo) 316 | symbols = mta.symbols_get(group="NON_EXISTENT_GROUP") 317 | assert symbols is not None 318 | assert isinstance(symbols, tuple) 319 | assert len(symbols) == 0 320 | 321 | 322 | def test_symbols_total(connected): 323 | with connected: 324 | total = mta.symbols_total() 325 | assert total is not None 326 | assert isinstance(total, int) 327 | 328 | 329 | def test_terminal_info(connected): 330 | with connected: 331 | info = mta.terminal_info() 332 | assert isinstance(info, mta.TerminalInfo) 333 | 334 | 335 | # HELPERS 336 | def test_as_dict_all(connected): 337 | from pymt5adapter import dictify 338 | from typing import Iterable 339 | # from time import perf_counter_ns 340 | 341 | def no_namedtuples(item): 342 | if isinstance(item, Iterable) and not isinstance(item, str): 343 | for i in item: 344 | if not no_namedtuples(i): 345 | return False 346 | if isinstance(item, dict): 347 | for k, v in item.items(): 348 | if not no_namedtuples(v): 349 | return False 350 | if hasattr(item, '_asdict'): 351 | return False 352 | return True 353 | 354 | with connected: 355 | deals = mta.history_deals_get()[:3] 356 | mutated_deals = dictify(deals) 357 | assert len(deals) == len(mutated_deals) 358 | assert all(isinstance(d, dict) for d in mutated_deals) 359 | symbols = mta.symbols_get() 360 | # b = perf_counter_ns() 361 | symbols = dictify(symbols) 362 | # total = perf_counter_ns() - b 363 | assert no_namedtuples(symbols) 364 | # print(f"as_dict_all_time = {total / 1000}") 365 | 366 | 367 | def test_borg_state_class(): 368 | from pymt5adapter.state import _GlobalState 369 | s1 = _GlobalState() 370 | s2 = _GlobalState() 371 | s1.raise_on_errors = True 372 | s2.raise_on_errors = False 373 | assert not s1.raise_on_errors 374 | 375 | 376 | def test_raise_on_errors(): 377 | with mta.connected(raise_on_errors=True): 378 | with pytest.raises(mta.MT5Error) as e: 379 | _ = mta.history_orders_total(',', ',') 380 | # does not raise 381 | with mta.connected(raise_on_errors=False): 382 | try: 383 | _ = mta.history_orders_total(',', ',') 384 | except mta.MT5Error as e: 385 | pytest.fail() 386 | with mta.connected(raise_on_errors=True) as conn: 387 | conn.raise_on_errors = False 388 | try: 389 | _ = mta.history_orders_total(',', ',') 390 | except mta.MT5Error as e: 391 | pytest.fail() 392 | with mta.connected(raise_on_errors=True): 393 | try: 394 | _ = mta.history_orders_total(',', ',') 395 | except mta.MT5Error as e: 396 | print(e) 397 | 398 | 399 | # NEW STUFF 400 | def test_trade_class(connected): 401 | from pymt5adapter.trade import Trade 402 | with connected: 403 | trade = Trade(symbol=first_symbol(), magic=1234) 404 | res = trade.buy(1.0) 405 | assert isinstance(res, mta.OrderSendResult) 406 | 407 | 408 | if __name__ == "__main__": 409 | pass 410 | --------------------------------------------------------------------------------