├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── examples ├── cli.py ├── ddeclient.py ├── ddefeeder.py ├── externaldeal.py └── server.py ├── pymt5 ├── __init__.py └── pymt5.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /dist/ 3 | /pymt5.egg-info/ 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 DevCartel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyMT5 2 | [![version](https://img.shields.io/pypi/v/pymt5.svg)](https://pypi.org/project/pymt5) 3 | [![pyversion](https://img.shields.io/pypi/pyversions/pymt5.svg)](#) 4 | [![platform](https://img.shields.io/badge/platform-linux|%20win-lightgray.svg)](#platform-availability) 5 | [![license](https://img.shields.io/pypi/l/pymt5.svg)](https://github.com/devcartel/pymt5/blob/master/LICENSE.txt) 6 | [![downloads](https://img.shields.io/pypi/dm/pymt5.svg)](https://pypi.org/project/pymt5) 7 | [![Sponsor](https://img.shields.io/badge/Sponsor%20PyMT5-%2419.99%2Fmonth-orange.svg?style=social&logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CKHUBNTFNDCB8) 8 | 9 | Provides simplified, multithreaded, socket-based Python interfaces to MT5 gateways. PyMT5 requires a [DevCartel MT5 gateway](https://github.com/devcartel/devcartelgateway64) installed on the MT5 platform to work with PyMT5. 10 | 11 |

12 | application 13 |

14 | 15 | ## Installation 16 | PyMT5 supports both Python 2 and 3. Simply install from [PyPI](https://pypi.org/project/pymt5) using `pip`: 17 | 18 | pip install pymt5 19 | 20 | ## Example 21 | ```python 22 | import pymt5 23 | 24 | def onData(data): 25 | client = data.get('client_id') 26 | # Login 27 | if data.get('type') == '1': 28 | # Send heartbeat 29 | m.send(client, {'ver':'3','type':'6'}) 30 | # Send login OK response 31 | m.send(client, {'ver':'3', 32 | 'type':'1', 33 | 'login':data.get('login'), 34 | 'password':data.get('password'), 35 | 'res':'0'}) 36 | 37 | m = pymt5.PyMT5() 38 | m.onConnected = onConnected 39 | m.onDisconnected = onDisconnected 40 | m.onData = onData 41 | 42 | ``` 43 | 44 | Checkout more message [examples](https://github.com/devcartel/pymt5/tree/master/examples). 45 | 46 | ## API 47 | __pymt5.PyMT5([_host=''_], [_port=16838_])__ 48 | _host: str_ 49 | _port: int_ 50 | _➥return: object_ 51 | Starts a PyMT5 server and listening on a port defined by _port_. 52 | 53 | >> m = pymt5.PyMT5() 54 | 55 | Upon incoming connection from a gateway, PyMT5 stores client information in `pymt5.requests` in dict format as 56 | 57 | __pymt5.stop()__ 58 | Disconnects all MT5 gateway connections and stop the server. 59 | 60 | >> m.stop() 61 | 62 | __pymt5.broadcast(_data_)__ 63 | _data: dict_ 64 | Sends a message to all connected gateways. Consider using this when sending market data. 65 | 66 | >> #send a tick 67 | >> m.broadcast({'ver':'3','type':'4','symbol':'EURUSD.TEST','bank':'dc','bid':'1.2661','ask':'1.2665','last':'1.2665','volume':'1','datetime':'0'}) 68 | 69 | __pymt5.send(client_id, _data_)__ 70 | client_id: int 71 | _data: dict_ 72 | Sends a message to a connected gateway. 73 | 74 | >> #send heartbeat 75 | >> m.send(123145536110592, {'ver':'3','type':'6'}) 76 | 77 | __pymt5.disconnect(client_id)__ 78 | client_id: int 79 | Terminates a connection. 80 | 81 | >> m.disconnect(123145536110592) 82 | 83 | __pymt5.onConnected(client_info)__ 84 | client_info: dict 85 | A callback `onConnected`, if assigned, is called upon a successful connection from a client. Client information can be accessed from `client_info`'s values as `client_id`, `client_address` and `client_port`. 86 | 87 | >> def onConnected(client_info): 88 | >> print(str(client_info)) 89 | >> # print {'client_port': 64941, 'client_address': '127.0.0.1', 'client_id': 123145536110592} 90 | >> 91 | >> m = pymt5.PyMT5() 92 | >> m.onConnected = onConnected 93 | 94 | __pymt5.onDisconnected(client_info)__ 95 | client_info: dict 96 | A callback `onDisconnected`, if assigned, is called upon a disconnection from a client. Client information can be accessed from `client_info`'s values as `client_id`, `client_address` and `client_port`. 97 | 98 | >> def onDisonnected(client_info): 99 | >> print(str(client_info)) 100 | >> 101 | >> m = pymt5.PyMT5() 102 | >> m.onDisconnected = onDisconnected 103 | 104 | __pymt5.onData(_data_)__ 105 | _data: dict_ 106 | A callback `onData`, if assigned, is called upon receiving messages from gateways. See [Data Format](#data-format) for more information. 107 | 108 | >> def onData(data): 109 | >> print(json.dumps(data)) 110 | >> 111 | >> m = pymt5.PyMT5() 112 | >> m.onData = onData 113 | 114 | ## Data Format 115 | Data is to be composed as a dict with key/value defined below to be sent and received from a gateway. 116 | 117 | | Data type | Header | Tags | 118 | | ------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 119 | | Login | `'ver':'3','type':1'` | `'login'`,`'password'`,`'res'` | 120 | | Logout | `'ver':'3','type':2'` | _None_ | 121 | | Symbol | `'ver':'3','type':3'` | `'index'`,`'symbol'`,`'path'`,`'description'`,`'page'`,
`'currency_base'`,`'currency_profit'`,`'currency_margin'`,
`'digits'`,`'tick_flags'`,`'calc_mode'`,`'exec_mode'`,
`'chart_mode'`,`'fill_flags'`,`'expir_flags'`,`'tick_value'`,
`'tick_size'`,`'contract_size'`,`'volume_min'`,`'volume_max'`,
`'volume_step'`,`'market_depth'`,`'margin_flags'`,
`'margin_initial'`,`'margin_maintenance'`,`'margin_long'`,
`'margin_short'`,`'margin_limit'`,
`'margin_stop'`,`'margin_stop_limit'`,`'settlement_price'`,
`'price_limit_max'`,`'price_limit_min'`,`'time_start'`,
`'time_expiration'`,`'trade_mode'` | 122 | | Tick | `'ver':'3','type':4'` | `'symbol'`,`'bank'`,`'bid'`,`'ask'`,`'last'`,`'volume'`,`'datetime'` | 123 | | Order | `'ver':'3','type':5'` | `'symbol'`,`'bank'`,`'bid'`,`'ask'`,`'last'`,`'volume'`,`'datetime'`,
`'order_action'`,`'state'`,`'order'`,`'exchange_id'`,
`'custom_data'`,`'request_id'`,`'symbol'`,`'login'`,
`'type_order'`,`'type_time'`,`'type_fill'`,`'action'`,`'price_order'`,
`'price_sl'`,`'price_tp'`,`'price_tick_bid'`,`'price_tick_ask'`,
`'volume'`,`'expiration_time'`,`'result'` | 124 | | Heartbeat | `'ver':'3','type':6'` | _None_ | 125 | | Deal | `'ver':'3','type':7'` | `'exchange_id'`,`'order'`,`'symbol'`,`'login'`,`'type_deal'`,
`'volume'`,`'volume_rem'`,`'price'`,`'position'` | 126 | | External Deal | `'ver':'3','type':50'` | `'exchange_id'`,`'order'`,`'symbol'`,`'login'`,`'type_deal'`,
`'volume'`,`'volume_rem'`,`'price'`,`'datetime'`,`'comment'` | 127 | 128 | ## Support 129 | * Get a [DevCartel MT5 Gateway](http://devcartel.com/devcartelgateway64) in order to work with PyMT5 130 | * Report an issue in [issue tracker](https://github.com/devcartel/pymt5/issues) 131 | 132 | ## Changelog 133 | 1.4.0 134 | * 30 October 2022 135 | * Fix potential data loss due to data fragmentation 136 | 137 | 1.3.0 138 | * 8 August 2021 139 | * Fix parsing data buffer 140 | 141 | 1.2.0 142 | * 8 July 2019 143 | * Support for Python 3.7 144 | * Update support links 145 | * Add examples 146 | 147 | 1.1.0 148 | * 21 April 2018 149 | * Released on PyPI 150 | * Added README 151 | 152 | 1.0.0 153 | * 13 April 2018 154 | * Initial release 155 | -------------------------------------------------------------------------------- /examples/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') 3 | import pymt5 4 | import time 5 | import signal 6 | from collections import OrderedDict 7 | 8 | client = '' 9 | order = deal = OrderedDict() 10 | m = None 11 | 12 | def onConnected(client_info): 13 | global client 14 | client = client_info.get('client_id') 15 | print('[onConnected] client_address: ' + str(client_info)) 16 | 17 | def onDisconnected(client_info): 18 | global client 19 | client = '' 20 | print('[onDisconnected] client_address: ' + str(client_info)) 21 | 22 | def sendOrderConfirmed(client, order): 23 | order.update({'exchange_id':str(int(time.time()))}) 24 | if order['type_order'] == '0': 25 | order.update({'price_order':'1.2665'}) 26 | else: 27 | order.update({'price_order':'1.2661'}) 28 | order.update({'state':'1'}) 29 | order.update({'result':'10009'}) 30 | m.send(client, order) 31 | 32 | def sendOrderPlaced(client, order): 33 | order.update({'state':'2'}) 34 | order.update({'result':'10008'}) 35 | m.send(client, order) 36 | 37 | def sendOrderNew(client, order): 38 | order.update({'state':'3'}) 39 | order.update({'result':'1'}) 40 | m.send(client, order) 41 | 42 | def sendOrderDeal(client, order): 43 | order.update({'state':'5'}) 44 | order.update({'result':'1'}) 45 | m.send(client, order) 46 | 47 | def sendOrderRejectNew(client, order): 48 | order.update({'state':'4'}) 49 | order.update({'result':'10006'}) 50 | m.send(client, order) 51 | 52 | def sendOrderCancel(client, order): 53 | order.update({'state':'10'}) 54 | order.update({'result':'10007'}) 55 | m.send(client, order) 56 | 57 | def sendOrderComplete(client, order): 58 | order.update({'state':'20'}) 59 | order.update({'result':'10009'}) 60 | m.send(client, order) 61 | 62 | def sendDeal(client, order, price): 63 | global deal 64 | deal = OrderedDict([('ver','3'), 65 | ('type','7'), 66 | ('exchange_id',str(int(time.time()))), 67 | ('order',order['order']), 68 | ('symbol',order['symbol']), 69 | ('login',order['login']), 70 | ('type_deal',order['type_order']), 71 | ('volume',order['volume']), 72 | ('volume_rem','0'), 73 | ('price',price)]) 74 | m.send(client, deal) 75 | 76 | def sendDealExternal(client): 77 | m.send(client, OrderedDict([('ver','3'),('type','50'),('exchange_id',str(int(time.time()))),('order','0'),('symbol','XAUUSD.G9'),('login','1000014'),('type_deal','0'),('volume','1'),('volume_rem','0'),('price','1344.80')])) 78 | 79 | def sendTick(): 80 | m.broadcast(OrderedDict([('ver','3'),('type','4'),('symbol','EURUSD.TEST'),('bank','dc'),('bid','1.2661'),('ask','1.2665'),('last','1.2665'),('volume','1'),('datetime','0')])) 81 | 82 | def onData(data): 83 | client = data.get('client_id') 84 | # Login 85 | if data.get('type') == '1': 86 | print('[onData] data: ' + str(data)) 87 | # Send heartbeat 88 | m.send(client, OrderedDict([('ver','3'), 89 | ('type','6')])) 90 | # Send login OK response 91 | m.send(client, OrderedDict([('ver','3'), 92 | ('type','1'), 93 | ('login',data.get('login')), 94 | ('password',data.get('password')), 95 | ('res','0')])) 96 | # Sync a symbol 97 | time.sleep(0.5) 98 | m.send(client, OrderedDict([('ver','3'), 99 | ('type','3'), 100 | ('index','0'), 101 | ('symbol','EURUSD.TEST'), 102 | ('path','TestExchange'), 103 | ('description','test'), 104 | ('page','http://www.google.co/finance?q=EURUSD.TEST'), 105 | ('currency_base','EUR'), 106 | ('currency_profit','USD'), 107 | ('currency_margin','USD'), 108 | ('digits','4'), 109 | ('tick_flags','7'), 110 | ('calc_mode','0'), 111 | ('exec_mode','2'), 112 | ('chart_mode','0'), 113 | ('fill_flags','3'), 114 | ('expir_flags','15'), 115 | ('tick_value','0.00000'), 116 | ('tick_size','0.00000'), 117 | ('contract_size','100000.00000'), 118 | ('volume_min','0.00000'), 119 | ('volume_max','10000.00000'), 120 | ('volume_step','0.01000'), 121 | ('market_depth','0'), 122 | ('margin_flags','0'), 123 | ('margin_initial','0.00000'), 124 | ('margin_maintenance','0.00000'), 125 | ('margin_long','1.00000'), 126 | ('margin_short','1.00000'), 127 | ('margin_limit','0.00000'), 128 | ('margin_stop','0.00000'), 129 | ('margin_stop_limit','0.00000'), 130 | ('settlement_price','1.22804'), 131 | ('price_limit_max','0.00000'), 132 | ('price_limit_min','0.00000'), 133 | ('time_start','0'), 134 | ('time_expiration','0'), 135 | ('trade_mode','4')])) 136 | m.send(client, OrderedDict([('ver','3'), 137 | ('type','3'), 138 | ('index','0'), 139 | ('symbol','XAUUSD.G9'), 140 | ('path','TestExchange'), 141 | ('description','test'), 142 | ('page','http://www.google.co/finance?q=XAUUSD.TEST'), 143 | ('currency_base','USD'), 144 | ('currency_profit','USD'), 145 | ('currency_margin','USD'), 146 | ('digits','2'), 147 | ('tick_flags','7'), 148 | ('calc_mode','0'), 149 | ('exec_mode','2'), 150 | ('chart_mode','0'), 151 | ('fill_flags','3'), 152 | ('expir_flags','15'), 153 | ('tick_value','0.00000'), 154 | ('tick_size','0.00000'), 155 | ('contract_size','100'), 156 | ('volume_min','0.10'), 157 | ('volume_max','100.00'), 158 | ('volume_step','0.01'), 159 | ('market_depth','0'), 160 | ('margin_flags','0'), 161 | ('margin_initial','0.00'), 162 | ('margin_maintenance','0.00'), 163 | ('margin_long','1.00'), 164 | ('margin_short','1.00'), 165 | ('margin_limit','0.00'), 166 | ('margin_stop','0.00'), 167 | ('margin_stop_limit','0.00'), 168 | ('settlement_price','0.00'), 169 | ('price_limit_max','0.00'), 170 | ('price_limit_min','0.00'), 171 | ('time_start','0'), 172 | ('time_expiration','0'), 173 | ('trade_mode','4')])) 174 | # Send a tick 175 | sendTick() 176 | # New order 177 | elif data.get('type') == '5' and data.get('state') == '0': 178 | print('[onData] data: ' + str(data)) 179 | global deal 180 | global order 181 | order = data 182 | del order['client_id'] 183 | # sendOrderConfirmed(client, order) 184 | # sendOrderPlaced(client, order) 185 | # sendOrderNew(client, order) 186 | # time.sleep(0.1) 187 | 188 | m = pymt5.PyMT5() 189 | m.onConnected = onConnected 190 | m.onDisconnected = onDisconnected 191 | m.onData = onData 192 | 193 | 194 | 195 | # Flow on new order 196 | # sendOrderPlaced -> sendOrderNew -> sendDeal 197 | 198 | #m.send(client, OrderedDict([('ver','3'),('type','7'),('exchange_id','3'),('order','4056'),('symbol','EURUSD.TEST'),('login','1000014'),('type_deal','0'),('volume','1'),('volume_rem','1'),('price','1.2665')])) 199 | # 200 | #OrderedDict([('ver', '3'), ('type', '5'), ('order_action', '1'), ('state', '0'), ('order', '4059'), ('exchange_id', '0'), ('custom_data', '0'), ('request_id', '12995002'), ('symbol', 'EURUSD.TEST'), ('login', '1000014'), ('type_order', '0'), ('type_time', '0'), ('action', '3'), ('price_order', '0.00000'), ('price_sl', '0.00000'), ('price_tp', '0.00000'), ('price_tick_bid', '0.00000'), ('price_tick_ask', '0.00000'), ('volume', '0'), ('expiration_time', '0'), ('result', '0'), ('client_id', 140558617073408)]) 201 | # 202 | #m.send(client, OrderedDict([('ver', '3'), ('type', '5'), ('order_action', '1'), ('state', '2'), ('order', '4059'), ('exchange_id', '2'), ('custom_data', '0'), ('request_id', '12995002'), ('symbol', 'EURUSD.TEST'), ('login', '1000014'), ('type_order', '0'), ('type_time', '0'), ('action', '3'), ('price_order', '0.00000'), ('price_sl', '0.00000'), ('price_tp', '0.00000'), ('price_tick_bid', '0.00000'), ('price_tick_ask', '0.00000'), ('volume', '0'), ('expiration_time', '0'), ('result', '0')])) 203 | 204 | # ORDER_STATE_UNKNOWN =0, // undefined state 205 | # ORDER_STATE_CONFIRMED =1, // order confirmed 206 | # ORDER_STATE_REQUEST_PLACED =2, // order generation request placed 207 | # ORDER_STATE_NEW =3, // create a new order 208 | # ORDER_STATE_REJECT_NEW =4, // order rejected 209 | # ORDER_STATE_DEAL =5, // a deal by order sent 210 | # ORDER_STATE_REQUEST_MODIFY =6, // order modification request received 211 | # ORDER_STATE_MODIFY =7, // order modified 212 | # ORDER_STATE_REJECT_MODIFY =8, // order modification rejected 213 | # ORDER_STATE_REQUEST_CANCEL =9, // order cancelation request received 214 | # ORDER_STATE_CANCEL =10, // order canceled 215 | # ORDER_STATE_REJECT_CANCEL =11, // order cancelation rejected 216 | # ORDER_STATE_COMPLETED =20 // operation on the order complete, can be removed from the queue 217 | 218 | #define MSG_TAG_ORDER_ACTION_TYPE "order_action=" // order action 219 | #define MSG_TAG_ORDER_STATE "state=" // order state 220 | #define MSG_TAG_ORDER_MT_ID "order=" // order ticket 221 | #define MSG_TAG_ORDER_EXCHANGE_ID "exchange_id=" // order id in external system 222 | #define MSG_TAG_ORDER_CUSTOM_DATA "custom_data=" // custom data 223 | #define MSG_TAG_ORDER_REQUEST_ID "request_id=" // request id 224 | #define MSG_TAG_ORDER_SYMBOL "symbol=" // symbol 225 | #define MSG_TAG_ORDER_LOGIN "login=" // client's login 226 | #define MSG_TAG_ORDER_TYPE_ORDER "type_order=" // order type 227 | #define MSG_TAG_ORDER_TYPE_TIME "type_time=" // order expiration type 228 | #define MSG_TAG_ORDER_ACTION "action=" // action 229 | #define MSG_TAG_ORDER_PRICE_ORDER "price_order=" // order price 230 | #define MSG_TAG_ORDER_PRICE_SL "price_sl=" // Stop Loss level 231 | #define MSG_TAG_ORDER_PRICE_TP "price_tp=" // Take Profit level 232 | #define MSG_TAG_ORDER_PRICE_TICK_BID "price_tick_bid=" // symbol bid price in external trading system 233 | #define MSG_TAG_ORDER_PRICE_TICK_ASK "price_tick_ask=" // symbol ask price in external trading system 234 | #define MSG_TAG_ORDER_VOLUME "volume=" // order volume 235 | #define MSG_TAG_ORDER_EXPIRATION_TIME "expiration_time=" // expiration time 236 | #define MSG_TAG_ORDER_RESULT "result=" // result of message processing 237 | 238 | #//+------------------------------------------------------------------+ 239 | #//| Deal message tags | 240 | #//+------------------------------------------------------------------+ 241 | #define MSG_TAG_DEAL_EXCHANGE_ID "exchange_id=" // deal id in external system 242 | #define MSG_TAG_DEAL_ORDER "order=" // order ticket 243 | #define MSG_TAG_DEAL_SYMBOL "symbol=" // symbol 244 | #define MSG_TAG_DEAL_LOGIN "login=" // login 245 | #define MSG_TAG_DEAL_TYPE "type_deal=" // action 246 | #define MSG_TAG_DEAL_VOLUME "volume=" // volume 247 | #define MSG_TAG_DEAL_VOLUME_REM "volume_rem=" // volume remaind 248 | #define MSG_TAG_DEAL_PRICE "price=" // price 249 | 250 | 251 | #//--- form the string value of order action type 252 | # switch(order.action) 253 | # { 254 | # case IMTRequest::TA_PRICE : res.Assign(L"prices for"); break; 255 | # case IMTRequest::TA_REQUEST : res.Assign(L"request"); break; 256 | # case IMTRequest::TA_INSTANT : res.Assign(L"instant"); break; 257 | # case IMTRequest::TA_MARKET : res.Assign(L"market"); break; 258 | # case IMTRequest::TA_EXCHANGE : res.Assign(L"exchange"); break; 259 | # case IMTRequest::TA_PENDING : res.Assign(L"pending"); break; 260 | # case IMTRequest::TA_SLTP : res.Assign(L"modify"); break; 261 | # case IMTRequest::TA_MODIFY : res.Assign(L"modify"); break; 262 | # case IMTRequest::TA_REMOVE : res.Assign(L"cancel"); break; 263 | # case IMTRequest::TA_ACTIVATE : res.Assign(L"activate"); break; 264 | # case IMTRequest::TA_ACTIVATE_SL : res.Assign(L"activate stop loss"); break; 265 | # case IMTRequest::TA_ACTIVATE_TP : res.Assign(L"activate take profit"); break; 266 | # case IMTRequest::TA_ACTIVATE_STOPLIMIT : res.Assign(L"activate stop-limit order"); break; 267 | # case IMTRequest::TA_STOPOUT_ORDER : res.Assign(L"delete stop-out order"); break; 268 | # case IMTRequest::TA_STOPOUT_POSITION : res.Assign(L"close stop-out position"); break; 269 | # case IMTRequest::TA_EXPIRATION : res.Assign(L"expire"); break; 270 | # default : res.Assign(L"order"); break; 271 | # } 272 | 273 | #//+------------------------------------------------------------------+ 274 | #//| Position message tags | 275 | #//+------------------------------------------------------------------+ 276 | #define MSG_TAG_POSITION_SYMBOL "pos_symbol=" // symbol 277 | #define MSG_TAG_POSITION_LOGIN "pos_login=" // client's login 278 | #define MSG_TAG_POSITION_PRICE "pos_price=" // position opening price 279 | #define MSG_TAG_POSITION_VOLUME "pos_volume=" // position volume 280 | #define MSG_TAG_POSITION_DIGITS "pos_digits=" // price digits 281 | -------------------------------------------------------------------------------- /examples/ddeclient.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | import sys 4 | from ctypes import c_int, c_double, c_char_p, c_void_p, c_ulong, c_char, pointer, cast 5 | from ctypes import windll, byref, create_string_buffer, Structure, sizeof 6 | from ctypes import POINTER, WINFUNCTYPE 7 | from ctypes.wintypes import BOOL, HWND, MSG, DWORD, BYTE, INT, LPCWSTR, UINT, ULONG, LPCSTR 8 | 9 | # DECLARE_HANDLE(name) typedef void *name; 10 | HCONV = c_void_p # = DECLARE_HANDLE(HCONV) 11 | HDDEDATA = c_void_p # = DECLARE_HANDLE(HDDEDATA) 12 | HSZ = c_void_p # = DECLARE_HANDLE(HSZ) 13 | LPBYTE = c_char_p # POINTER(BYTE) 14 | LPDWORD = POINTER(DWORD) 15 | LPSTR = c_char_p 16 | ULONG_PTR = c_ulong 17 | 18 | # See windows/ddeml.h for declaration of struct CONVCONTEXT 19 | PCONVCONTEXT = c_void_p 20 | 21 | # DDEML errors 22 | DMLERR_NO_ERROR = 0x0000 # No error 23 | DMLERR_ADVACKTIMEOUT = 0x4000 # request for synchronous advise transaction timed out 24 | DMLERR_DATAACKTIMEOUT = 0x4002 # request for synchronous data transaction timed out 25 | DMLERR_DLL_NOT_INITIALIZED = 0x4003 # DDEML functions called without iniatializing 26 | DMLERR_EXECACKTIMEOUT = 0x4006 # request for synchronous execute transaction timed out 27 | DMLERR_NO_CONV_ESTABLISHED = 0x400a # client's attempt to establish a conversation has failed (can happen during DdeConnect) 28 | DMLERR_POKEACKTIMEOUT = 0x400b # A request for a synchronous poke transaction has timed out. 29 | DMLERR_POSTMSG_FAILED = 0x400c # An internal call to the PostMessage function has failed. 30 | DMLERR_SERVER_DIED = 0x400e 31 | 32 | # Predefined Clipboard Formats 33 | CF_TEXT = 1 34 | CF_BITMAP = 2 35 | CF_METAFILEPICT = 3 36 | CF_SYLK = 4 37 | CF_DIF = 5 38 | CF_TIFF = 6 39 | CF_OEMTEXT = 7 40 | CF_DIB = 8 41 | CF_PALETTE = 9 42 | CF_PENDATA = 10 43 | CF_RIFF = 11 44 | CF_WAVE = 12 45 | CF_UNICODETEXT = 13 46 | CF_ENHMETAFILE = 14 47 | CF_HDROP = 15 48 | CF_LOCALE = 16 49 | CF_DIBV5 = 17 50 | CF_MAX = 18 51 | 52 | # DDE constants for wStatus field 53 | DDE_FACK = 0x8000 54 | DDE_FBUSY = 0x4000 55 | DDE_FDEFERUPD = 0x4000 56 | DDE_FACKREQ = 0x8000 57 | DDE_FRELEASE = 0x2000 58 | DDE_FREQUESTED = 0x1000 59 | DDE_FAPPSTATUS = 0x00FF 60 | DDE_FNOTPROCESSED = 0x0000 61 | 62 | DDE_FACKRESERVED = (~(DDE_FACK | DDE_FBUSY | DDE_FAPPSTATUS)) 63 | DDE_FADVRESERVED = (~(DDE_FACKREQ | DDE_FDEFERUPD)) 64 | DDE_FDATRESERVED = (~(DDE_FACKREQ | DDE_FRELEASE | DDE_FREQUESTED)) 65 | DDE_FPOKRESERVED = (~(DDE_FRELEASE)) 66 | 67 | # DDEML Transaction class flags 68 | XTYPF_NOBLOCK = 0x0002 69 | XTYPF_NODATA = 0x0004 70 | XTYPF_ACKREQ = 0x0008 71 | 72 | XCLASS_MASK = 0xFC00 73 | XCLASS_BOOL = 0x1000 74 | XCLASS_DATA = 0x2000 75 | XCLASS_FLAGS = 0x4000 76 | XCLASS_NOTIFICATION = 0x8000 77 | 78 | XTYP_ERROR = (0x0000 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK) 79 | XTYP_ADVDATA = (0x0010 | XCLASS_FLAGS) 80 | XTYP_ADVREQ = (0x0020 | XCLASS_DATA | XTYPF_NOBLOCK) 81 | XTYP_ADVSTART = (0x0030 | XCLASS_BOOL) 82 | XTYP_ADVSTOP = (0x0040 | XCLASS_NOTIFICATION) 83 | XTYP_EXECUTE = (0x0050 | XCLASS_FLAGS) 84 | XTYP_CONNECT = (0x0060 | XCLASS_BOOL | XTYPF_NOBLOCK) 85 | XTYP_CONNECT_CONFIRM = (0x0070 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK) 86 | XTYP_XACT_COMPLETE = (0x0080 | XCLASS_NOTIFICATION ) 87 | XTYP_POKE = (0x0090 | XCLASS_FLAGS) 88 | XTYP_REGISTER = (0x00A0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK ) 89 | XTYP_REQUEST = (0x00B0 | XCLASS_DATA ) 90 | XTYP_DISCONNECT = (0x00C0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK ) 91 | XTYP_UNREGISTER = (0x00D0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK ) 92 | XTYP_WILDCONNECT = (0x00E0 | XCLASS_DATA | XTYPF_NOBLOCK) 93 | XTYP_MONITOR = (0x00F0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK) 94 | 95 | XTYP_MASK = 0x00F0 96 | XTYP_SHIFT = 4 97 | 98 | # DDE Timeout constants 99 | TIMEOUT_ASYNC = 0xFFFFFFFF 100 | 101 | # DDE Application command flags / Initialization flag (afCmd) 102 | APPCMD_CLIENTONLY = 0x00000010 103 | 104 | # Code page for rendering string. 105 | CP_WINANSI = 1004 # default codepage for windows & old DDE convs. 106 | CP_WINUNICODE = 1200 107 | 108 | # Declaration 109 | DDECALLBACK = WINFUNCTYPE(HDDEDATA, UINT, UINT, HCONV, HSZ, HSZ, HDDEDATA, ULONG_PTR, ULONG_PTR) 110 | 111 | # PyZDDE specific globals 112 | number_of_apps_communicating = 0 # to keep an account of the number of zemax 113 | # server objects --'ZEMAX', 'ZEMAX1' etc 114 | 115 | class CreateServer(object): 116 | """This is really just an interface class so that PyZDDE can use either the 117 | current dde code or the pywin32 transparently. This object is created only 118 | once. The class name cannot be anything else if compatibility has to be maintained 119 | between pywin32 and this dde code. 120 | """ 121 | def __init__(self): 122 | self.serverName = 'None' 123 | 124 | def Create(self, client): 125 | """Set a DDE client that will communicate with the DDE server 126 | 127 | Parameters 128 | ---------- 129 | client : string 130 | Name of the DDE client, most likely this will be 'ZCLIENT' 131 | """ 132 | self.clientName = client # shall be used in `CreateConversation` 133 | 134 | def Shutdown(self, createConvObj): 135 | """The shutdown should ideally be requested only once per CreateConversation 136 | object by the PyZDDE module, but for ALL CreateConversation objects, if there 137 | are more than one. If multiple CreateConversation objects were created and 138 | then not cleared, there will be memory leak, and eventually the program will 139 | error out when run multiple times 140 | 141 | Parameters 142 | ---------- 143 | createConvObj : CreateConversation object 144 | 145 | Exceptions 146 | ---------- 147 | An exception occurs if a Shutdown is attempted with a CreateConvObj that 148 | doesn't have a conversation object (connection with ZEMAX established) 149 | """ 150 | global number_of_apps_communicating 151 | #print("Shutdown requested by {}".format(repr(createConvObj))) # for debugging 152 | if number_of_apps_communicating > 0: 153 | #print("Deleting object ...") # for debugging 154 | createConvObj.ddec.__del__() 155 | number_of_apps_communicating -=1 156 | 157 | 158 | class CreateConversation(object): 159 | """This is really just an interface class so that PyZDDE can use either the 160 | current dde code or the pywin32 transparently. 161 | 162 | Multiple objects of this type may be instantiated depending upon the 163 | number of simultaneous channels of communication with Zemax that the user 164 | program wants to establish using `ln = pyz.PyZDDE()` followed by `ln.zDDEInit()` 165 | calls. 166 | """ 167 | def __init__(self, ddeServer): 168 | """ 169 | Parameters 170 | ---------- 171 | ddeServer : 172 | d 173 | """ 174 | self.ddeClientName = ddeServer.clientName 175 | self.ddeServerName = 'None' 176 | self.ddetimeout = 50 # default dde timeout = 50 seconds 177 | 178 | def ConnectTo(self, appName, data=None): 179 | """Exceptional error is handled in zdde Init() method, so the exception 180 | must be re-raised""" 181 | global number_of_apps_communicating 182 | self.ddeServerName = appName 183 | try: 184 | self.ddec = DDEClient(self.ddeServerName, self.ddeClientName) # establish conversation 185 | except DDEError: 186 | raise 187 | else: 188 | number_of_apps_communicating +=1 189 | #print("Number of apps communicating: ", number_of_apps_communicating) # for debugging 190 | 191 | def Request(self, item, timeout=None): 192 | """Request DDE client 193 | timeout in seconds 194 | Note ... handle the exception within this function. 195 | """ 196 | if not timeout: 197 | timeout = self.ddetimeout 198 | try: 199 | reply = self.ddec.request(item, int(timeout*1000)) # convert timeout into milliseconds 200 | except DDEError: 201 | err_str = str(sys.exc_info()[1]) 202 | error = err_str[err_str.find('err=')+4:err_str.find('err=')+10] 203 | if error == hex(DMLERR_DATAACKTIMEOUT): 204 | print("TIMEOUT REACHED. Please use a higher timeout.\n") 205 | if (sys.version_info > (3, 0)): #this is only evaluated in case of an error 206 | reply = b'-998' #Timeout error value 207 | else: 208 | reply = '-998' #Timeout error value 209 | return reply 210 | 211 | def RequestArrayTrace(self, ddeRayData, timeout=None): 212 | """Request bulk ray tracing 213 | 214 | Parameters 215 | ---------- 216 | ddeRayData : the ray data for array trace 217 | """ 218 | pass 219 | # TO DO!!! 220 | # 1. Assign proper timeout as in Request() function 221 | # 2. Create the rayData structure conforming to ctypes structure 222 | # 3. Process the reply and return ray trace data 223 | # 4. Handle errors 224 | #reply = self.ddec.poke("RayArrayData", rayData, timeout) 225 | 226 | def SetDDETimeout(self, timeout): 227 | """Set DDE timeout 228 | timeout : timeout in seconds 229 | """ 230 | self.ddetimeout = timeout 231 | 232 | def GetDDETimeout(self): 233 | """Returns the current timeout value in seconds 234 | """ 235 | return self.ddetimeout 236 | 237 | 238 | def get_winfunc(libname, funcname, restype=None, argtypes=(), _libcache={}): 239 | """Retrieve a function from a library/DLL, and set the data types.""" 240 | if libname not in _libcache: 241 | _libcache[libname] = windll.LoadLibrary(libname) 242 | func = getattr(_libcache[libname], funcname) 243 | func.argtypes = argtypes 244 | func.restype = restype 245 | return func 246 | 247 | class DDE(object): 248 | """Object containing all the DDEML functions""" 249 | AccessData = get_winfunc("user32", "DdeAccessData", LPBYTE, (HDDEDATA, LPDWORD)) 250 | ClientTransaction = get_winfunc("user32", "DdeClientTransaction", HDDEDATA, (LPBYTE, DWORD, HCONV, HSZ, UINT, UINT, DWORD, LPDWORD)) 251 | Connect = get_winfunc("user32", "DdeConnect", HCONV, (DWORD, HSZ, HSZ, PCONVCONTEXT)) 252 | CreateDataHandle = get_winfunc("user32", "DdeCreateDataHandle", HDDEDATA, (DWORD, LPBYTE, DWORD, DWORD, HSZ, UINT, UINT)) 253 | CreateStringHandle = get_winfunc("user32", "DdeCreateStringHandleW", HSZ, (DWORD, LPCWSTR, UINT)) # Unicode version 254 | #CreateStringHandle = get_winfunc("user32", "DdeCreateStringHandleA", HSZ, (DWORD, LPCSTR, UINT)) # ANSI version 255 | Disconnect = get_winfunc("user32", "DdeDisconnect", BOOL, (HCONV,)) 256 | GetLastError = get_winfunc("user32", "DdeGetLastError", UINT, (DWORD,)) 257 | Initialize = get_winfunc("user32", "DdeInitializeW", UINT, (LPDWORD, DDECALLBACK, DWORD, DWORD)) # Unicode version of DDE initialize 258 | #Initialize = get_winfunc("user32", "DdeInitializeA", UINT, (LPDWORD, DDECALLBACK, DWORD, DWORD)) # ANSI version of DDE initialize 259 | FreeDataHandle = get_winfunc("user32", "DdeFreeDataHandle", BOOL, (HDDEDATA,)) 260 | FreeStringHandle = get_winfunc("user32", "DdeFreeStringHandle", BOOL, (DWORD, HSZ)) 261 | QueryString = get_winfunc("user32", "DdeQueryStringA", DWORD, (DWORD, HSZ, LPSTR, DWORD, c_int)) # ANSI version of QueryString 262 | UnaccessData = get_winfunc("user32", "DdeUnaccessData", BOOL, (HDDEDATA,)) 263 | Uninitialize = get_winfunc("user32", "DdeUninitialize", BOOL, (DWORD,)) 264 | 265 | class DDEError(RuntimeError): 266 | """Exception raise when a DDE error occures.""" 267 | def __init__(self, msg, idInst=None): 268 | if idInst is None: 269 | RuntimeError.__init__(self, msg) 270 | else: 271 | RuntimeError.__init__(self, "%s (err=%s)" % (msg, hex(DDE.GetLastError(idInst)))) 272 | 273 | class DDEClient(object): 274 | """The DDEClient class. 275 | 276 | Use this class to create and manage a connection to a service/topic. To get 277 | classbacks subclass DDEClient and overwrite callback.""" 278 | 279 | def __init__(self, service, topic): 280 | """Create a connection to a service/topic.""" 281 | self._idInst = DWORD(0) # application instance identifier. 282 | self._hConv = HCONV() 283 | 284 | self._callback = DDECALLBACK(self._callback) 285 | # Initialize and register application with DDEML 286 | res = DDE.Initialize(byref(self._idInst), self._callback, APPCMD_CLIENTONLY, 0) 287 | if res != DMLERR_NO_ERROR: 288 | raise DDEError("Unable to register with DDEML (err=%s)" % hex(res)) 289 | 290 | hszServName = DDE.CreateStringHandle(self._idInst, service, CP_WINUNICODE) 291 | hszTopic = DDE.CreateStringHandle(self._idInst, topic, CP_WINUNICODE) 292 | # Try to establish conversation with the Zemax server 293 | self._hConv = DDE.Connect(self._idInst, hszServName, hszTopic, PCONVCONTEXT()) 294 | DDE.FreeStringHandle(self._idInst, hszTopic) 295 | DDE.FreeStringHandle(self._idInst, hszServName) 296 | if not self._hConv: 297 | raise DDEError("Unable to establish a conversation with server", self._idInst) 298 | 299 | def __del__(self): 300 | """Cleanup any active connections and free all DDEML resources.""" 301 | if self._hConv: 302 | DDE.Disconnect(self._hConv) 303 | if self._idInst: 304 | DDE.Uninitialize(self._idInst) 305 | 306 | def advise(self, item, stop=False): 307 | """Request updates when DDE data changes.""" 308 | hszItem = DDE.CreateStringHandle(self._idInst, item, CP_WINUNICODE) 309 | hDdeData = DDE.ClientTransaction(LPBYTE(), 0, self._hConv, hszItem, CF_TEXT, XTYP_ADVSTOP if stop else XTYP_ADVSTART, TIMEOUT_ASYNC, LPDWORD()) 310 | DDE.FreeStringHandle(self._idInst, hszItem) 311 | if not hDdeData: 312 | raise DDEError("Unable to %s advise" % ("stop" if stop else "start"), self._idInst) 313 | DDE.FreeDataHandle(hDdeData) 314 | 315 | def execute(self, command): 316 | """Execute a DDE command.""" 317 | pData = c_char_p(command) 318 | cbData = DWORD(len(command) + 1) 319 | hDdeData = DDE.ClientTransaction(pData, cbData, self._hConv, HSZ(), CF_TEXT, XTYP_EXECUTE, TIMEOUT_ASYNC, LPDWORD()) 320 | if not hDdeData: 321 | raise DDEError("Unable to send command", self._idInst) 322 | DDE.FreeDataHandle(hDdeData) 323 | 324 | def request(self, item, timeout=5000): 325 | """Request data from DDE service.""" 326 | hszItem = DDE.CreateStringHandle(self._idInst, item, CP_WINUNICODE) 327 | #hDdeData = DDE.ClientTransaction(LPBYTE(), 0, self._hConv, hszItem, CF_TEXT, XTYP_REQUEST, timeout, LPDWORD()) 328 | pdwResult = DWORD(0) 329 | hDdeData = DDE.ClientTransaction(LPBYTE(), 0, self._hConv, hszItem, CF_TEXT, XTYP_REQUEST, timeout, byref(pdwResult)) 330 | DDE.FreeStringHandle(self._idInst, hszItem) 331 | if not hDdeData: 332 | raise DDEError("Unable to request item", self._idInst) 333 | 334 | if timeout != TIMEOUT_ASYNC: 335 | pdwSize = DWORD(0) 336 | pData = DDE.AccessData(hDdeData, byref(pdwSize)) 337 | if not pData: 338 | DDE.FreeDataHandle(hDdeData) 339 | raise DDEError("Unable to access data in request function", self._idInst) 340 | DDE.UnaccessData(hDdeData) 341 | else: 342 | pData = None 343 | DDE.FreeDataHandle(hDdeData) 344 | return pData 345 | 346 | def poke(self, item, data, timeout=5000): 347 | """Poke (unsolicited) data to DDE server""" 348 | hszItem = DDE.CreateStringHandle(self._idInst, item, CP_WINUNICODE) 349 | pData = c_char_p(data) 350 | cbData = DWORD(len(data) + 1) 351 | pdwResult = DWORD(0) 352 | #hData = DDE.CreateDataHandle(self._idInst, data, cbData, 0, hszItem, CP_WINUNICODE, 0) 353 | #hDdeData = DDE.ClientTransaction(hData, -1, self._hConv, hszItem, CF_TEXT, XTYP_POKE, timeout, LPDWORD()) 354 | hDdeData = DDE.ClientTransaction(pData, cbData, self._hConv, hszItem, CF_TEXT, XTYP_POKE, timeout, byref(pdwResult)) 355 | DDE.FreeStringHandle(self._idInst, hszItem) 356 | #DDE.FreeDataHandle(dData) 357 | if not hDdeData: 358 | print("Value of pdwResult: ", pdwResult) 359 | raise DDEError("Unable to poke to server", self._idInst) 360 | 361 | if timeout != TIMEOUT_ASYNC: 362 | pdwSize = DWORD(0) 363 | pData = DDE.AccessData(hDdeData, byref(pdwSize)) 364 | if not pData: 365 | DDE.FreeDataHandle(hDdeData) 366 | raise DDEError("Unable to access data in poke function", self._idInst) 367 | # TODO: use pdwSize 368 | DDE.UnaccessData(hDdeData) 369 | else: 370 | pData = None 371 | DDE.FreeDataHandle(hDdeData) 372 | return pData 373 | 374 | def callback(self, value, topic=None, item=None): 375 | """Callback function for advice.""" 376 | print("callback: %s: %s=%s" % (item.decode('euc-kr'), topic.decode('euc-kr'), value.decode('euc-kr'))) 377 | 378 | def _callback(self, wType, uFmt, hConv, hsz1, hsz2, hDdeData, dwData1, dwData2): 379 | """DdeCallback callback function for processing Dynamic Data Exchange (DDE) 380 | transactions sent by DDEML in response to DDE events 381 | 382 | Parameters 383 | ---------- 384 | wType : transaction type (UINT) 385 | uFmt : clipboard data format (UINT) 386 | hConv : handle to conversation (HCONV) 387 | hsz1 : handle to string (HSZ) 388 | hsz2 : handle to string (HSZ) 389 | hDDedata : handle to global memory object (HDDEDATA) 390 | dwData1 : transaction-specific data (DWORD) 391 | dwData2 : transaction-specific data (DWORD) 392 | 393 | Returns 394 | ------- 395 | ret : specific to the type of transaction (HDDEDATA) 396 | """ 397 | if wType == XTYP_ADVDATA: # value of the data item has changed [hsz1 = topic; hsz2 = item; hDdeData = data] 398 | dwSize = DWORD(0) 399 | pData = DDE.AccessData(hDdeData, byref(dwSize)) 400 | if pData: 401 | topic = create_string_buffer(b'\000' * 128) 402 | item = create_string_buffer(b'\000' * 128) 403 | DDE.QueryString(self._idInst, hsz1, topic, 128, CP_WINANSI) 404 | DDE.QueryString(self._idInst, hsz2, item, 128, CP_WINANSI) 405 | self.callback(pData, topic.value, item.value) 406 | DDE.UnaccessData(hDdeData) 407 | return DDE_FACK 408 | else: 409 | print("Error: AccessData returned NULL! (err = %s)"% (hex(DDE.GetLastError(self._idInst)))) 410 | if wType == XTYP_DISCONNECT: 411 | print("Disconnect notification received from server") 412 | 413 | return 0 414 | 415 | def WinMSGLoop(): 416 | """Run the main windows message loop.""" 417 | LPMSG = POINTER(MSG) 418 | LRESULT = c_ulong 419 | GetMessage = get_winfunc("user32", "GetMessageW", BOOL, (LPMSG, HWND, UINT, UINT)) 420 | TranslateMessage = get_winfunc("user32", "TranslateMessage", BOOL, (LPMSG,)) 421 | # restype = LRESULT 422 | DispatchMessage = get_winfunc("user32", "DispatchMessageW", LRESULT, (LPMSG,)) 423 | 424 | msg = MSG() 425 | lpmsg = byref(msg) 426 | while GetMessage(lpmsg, HWND(), 0, 0) > 0: 427 | TranslateMessage(lpmsg) 428 | DispatchMessage(lpmsg) 429 | 430 | if __name__ == "__main__": 431 | quotes = {} 432 | bid = DDEClient("WINROS", "bid") 433 | ask = DDEClient("WINROS", "ask") 434 | last = DDEClient("WINROS", "last") 435 | totalvol = DDEClient("WINROS", "totalvol") 436 | bid.advise("GC V19") 437 | ask.advise("GC V19") 438 | last.advise("GC V19") 439 | totalvol.advise("GC V19") 440 | WinMSGLoop() 441 | -------------------------------------------------------------------------------- /examples/ddefeeder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pymt5 3 | import time 4 | import signal 5 | from collections import OrderedDict 6 | import json 7 | exec(open('./ddeclient.py').read()) 8 | 9 | symbols = [ 10 | 11 | 'CRUDEOIL V19-MCX', 12 | 'GOLD Z19-MCX', 13 | 'BANKNIFTY U19-NSF', 14 | 15 | ] 16 | maps = { 17 | 18 | 'CRUDEOIL V19-MCX' : 'CRUDEOIL-1', 19 | 'GOLD Z19-MCX' : 'GOLD-1', 20 | 'BANKNIFTY U19-NSF' : 'BANKNIFTY-1', 21 | 22 | } 23 | clients = {} 24 | indices = {} 25 | quotes = {} 26 | 27 | ################## 28 | # PyMT5 handlers 29 | ################## 30 | def onConnected(client_info): 31 | client = client_info.get('client_id') 32 | clients[client] = client_info 33 | clients[client]['logged_in'] = False 34 | print("MT5 - onConnected - client_info:{}".format(str(client_info))) 35 | print("MT5 - onConnected - clients:{}".format(str(list(clients)))) 36 | 37 | def onDisconnected(client_info): 38 | client = client_info.get('client_id') 39 | del clients[client] 40 | print("MT5 - onDisconnected - client_info:{}".format(str(client_info))) 41 | print("MT5 - onDisconnected - clients:{}".format(str(list(clients)))) 42 | 43 | def sendSymbol(client, symbol): 44 | data = OrderedDict([('ver','3'), 45 | ('type','3'), 46 | ('index',str(indices[client])), 47 | ('symbol',symbol), 48 | ('path','DDE'), 49 | ('description','DDE'), 50 | ('page',''), 51 | ('currency_base',''), 52 | ('currency_profit',''), 53 | ('currency_margin',''), 54 | ('digits','4'), 55 | ('tick_flags','7'), 56 | ('calc_mode','0'), 57 | ('exec_mode','2'), 58 | ('chart_mode','0'), 59 | ('fill_flags','3'), 60 | ('expir_flags','15'), 61 | ('tick_value','0.00000'), 62 | ('tick_size','0.00000'), 63 | ('contract_size','100000.00000'), 64 | ('volume_min','0.01000'), 65 | ('volume_max','10000.00000'), 66 | ('volume_step','0.01000'), 67 | ('market_depth','0'), 68 | ('margin_flags','0'), 69 | ('margin_initial','0.00000'), 70 | ('margin_maintenance','0.00000'), 71 | ('margin_long','1.00000'), 72 | ('margin_short','1.00000'), 73 | ('margin_limit','0.00000'), 74 | ('margin_stop','0.00000'), 75 | ('margin_stop_limit','0.00000'), 76 | ('settlement_price','1.22804'), 77 | ('price_limit_max','0.00000'), 78 | ('price_limit_min','0.00000'), 79 | ('time_start','0'), 80 | ('time_expiration','0'), 81 | ('trade_mode','4')]) 82 | m.send(client, data) 83 | indices[client] += 1 84 | 85 | def onData(data): 86 | if data.get('type') == '1': 87 | onLogin(data) 88 | elif data.get('type') == '2': 89 | onLogout(data) 90 | elif data.get('type') == '6': 91 | onHeartbeat(data) 92 | else: 93 | print("MT5 - onData - data:{}".format(json.dumps(data))) 94 | 95 | def onSIGINT(signum, frame): 96 | global end 97 | end = True 98 | 99 | def onHeartbeat(data): 100 | print("MT5 - onHeartbeat - data:{}".format(json.dumps(data))) 101 | 102 | def onLogin(data): 103 | print("MT5 - onLogin - data:{}".format(json.dumps(data))) 104 | client = data.get('client_id') 105 | login = data.get('login') 106 | password = data.get('password') 107 | # Send login OK response 108 | if login and password: 109 | clients[client]['logged_in'] = True 110 | m.send(client, OrderedDict([('ver','3'), 111 | ('type','1'), 112 | ('login',login), 113 | ('password',password), 114 | ('res','0')])) 115 | # Send a test symbol to make gateway status online 116 | indices[client] = 0 117 | for symbol in symbols: 118 | sendSymbol(client, maps[symbol]) 119 | else: 120 | m.disconnect(client) 121 | del clients[client] 122 | print("MT5 - onLogin - not authorized - client_id:{}".format(client)) 123 | 124 | def onLogout(data): 125 | print("MT5 - onLogout - data:{}".format(json.dumps(data))) 126 | client = data.get('client_id') 127 | del clients[client] 128 | m.disconnect(client) 129 | print("MT5 - onLogout - data:{}".format(json.dumps(data))) 130 | print("MT5 - onLogout - clients:{}".format(str(list(clients)))) 131 | 132 | ###################### 133 | # DDE client callback 134 | ###################### 135 | class ESignal(DDEClient): 136 | def callback(self, value, topic=None, item=None): 137 | print("eSignal - %s - %s=%s" % (item.decode('euc-kr'), topic.decode('euc-kr'), value.decode('euc-kr'))) 138 | symbol = item.decode('euc-kr') 139 | #quotes[maps[symbol]]['datetime'] = str(int(round(time.time() * 1000))).replace('L','')[:-3] 140 | # Best bid/ask 141 | if topic.decode('euc-kr') == 'bid': 142 | quotes[maps[symbol]]['bid'] = value.decode('euc-kr') 143 | if topic.decode('euc-kr') == 'ask': 144 | quotes[maps[symbol]]['ask'] = value.decode('euc-kr') 145 | quotes[maps[symbol]]['last'] = '0' 146 | quotes[maps[symbol]]['volume'] = '0' 147 | print("MT5 - sendTick - data:{}".format(str(dict(quotes[maps[symbol]])))) 148 | m.broadcast(quotes[maps[symbol]]) 149 | # Last and volume 150 | if topic.decode('euc-kr') == 'last': 151 | quotes[maps[symbol]]['last'] = value.decode('euc-kr') 152 | if topic.decode('euc-kr') == 'tradesize': 153 | quotes[maps[symbol]]['bid'] = '' 154 | quotes[maps[symbol]]['ask'] = '' 155 | quotes[maps[symbol]]['volume'] = value.decode('euc-kr') 156 | print("MT5 - sendTick - data:{}".format(str(dict(quotes[maps[symbol]])))) 157 | m.broadcast(quotes[maps[symbol]]) 158 | 159 | ################## 160 | # Main 161 | ################## 162 | signal.signal(signal.SIGINT, onSIGINT) 163 | 164 | # MT5 gateway 165 | m = pymt5.PyMT5() 166 | m.onConnected = onConnected 167 | m.onDisconnected = onDisconnected 168 | m.onData = onData 169 | 170 | time.sleep(10) 171 | 172 | # eSignal DDE client 173 | bid = ESignal("WINROS", "bid") 174 | ask = ESignal("WINROS", "ask") 175 | last = ESignal("WINROS", "last") 176 | tradesize = ESignal("WINROS", "tradesize") 177 | for symbol in symbols: 178 | quotes[maps[symbol]] = OrderedDict([('ver','3'), 179 | ('type','4'), 180 | ('symbol',maps[symbol]), 181 | ('bank','esignal'), 182 | ('bid','0'), 183 | ('ask','0'), 184 | ('last','0'), 185 | ('volume','0'), 186 | ('datetime','0')]) 187 | bid.advise(symbol) 188 | ask.advise(symbol) 189 | last.advise(symbol) 190 | tradesize.advise(symbol) 191 | 192 | # Event loop 193 | end = False 194 | LPMSG = POINTER(MSG) 195 | LRESULT = c_ulong 196 | GetMessage = get_winfunc("user32", "GetMessageW", BOOL, (LPMSG, HWND, UINT, UINT)) 197 | TranslateMessage = get_winfunc("user32", "TranslateMessage", BOOL, (LPMSG,)) 198 | DispatchMessage = get_winfunc("user32", "DispatchMessageW", LRESULT, (LPMSG,)) 199 | msg = MSG() 200 | lpmsg = byref(msg) 201 | while GetMessage(lpmsg, HWND(), 0, 0) > 0 and not end: 202 | TranslateMessage(lpmsg) 203 | DispatchMessage(lpmsg) 204 | 205 | m.stop() 206 | -------------------------------------------------------------------------------- /examples/externaldeal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | sys.path.append('../') 5 | import pymt5 6 | import time 7 | import signal 8 | from collections import OrderedDict 9 | 10 | client = '' 11 | order = deal = OrderedDict() 12 | logged_in = False 13 | m = None 14 | 15 | def onConnected(client_info): 16 | global client 17 | client = client_info.get('client_id') 18 | print('[onConnected] client_address: ' + str(client_info)) 19 | 20 | def onDisconnected(client_info): 21 | global client 22 | client = '' 23 | print('[onDisconnected] client_address: ' + str(client_info)) 24 | 25 | def onData(data): 26 | global logged_in 27 | print('[onData] data: ' + str(data)) 28 | client = data.get('client_id') 29 | # Login 30 | if data.get('type') == '1': 31 | # Send heartbeat 32 | m.send(client, OrderedDict([('ver','3'), 33 | ('type','6')])) 34 | # Send login OK response 35 | m.send(client, OrderedDict([('ver','3'), 36 | ('type','1'), 37 | ('login',data.get('login')), 38 | ('password',data.get('password')), 39 | ('res','0')])) 40 | # Sync a symbol 41 | time.sleep(0.5) 42 | m.send(client, OrderedDict([('ver','3'), 43 | ('type','3'), 44 | ('index','0'), 45 | ('symbol','XAUUSD.a'), 46 | ('path','TestExchange'), 47 | ('description','test'), 48 | ('page','http://www.google.co/finance?q=XAUUSD.a'), 49 | ('currency_base','XAU'), 50 | ('currency_profit','USD'), 51 | ('currency_margin','USD'), 52 | ('digits','4'), 53 | ('tick_flags','7'), 54 | ('calc_mode','0'), 55 | ('exec_mode','2'), 56 | ('chart_mode','0'), 57 | ('fill_flags','3'), 58 | ('expir_flags','15'), 59 | ('tick_value','0.00000'), 60 | ('tick_size','0.00000'), 61 | ('contract_size','100000.00000'), 62 | ('volume_min','0.00000'), 63 | ('volume_max','10000.00000'), 64 | ('volume_step','0.01000'), 65 | ('market_depth','0'), 66 | ('margin_flags','0'), 67 | ('margin_initial','0.00000'), 68 | ('margin_maintenance','0.00000'), 69 | ('margin_long','1.00000'), 70 | ('margin_short','1.00000'), 71 | ('margin_limit','0.00000'), 72 | ('margin_stop','0.00000'), 73 | ('margin_stop_limit','0.00000'), 74 | ('settlement_price','1.22804'), 75 | ('price_limit_max','0.00000'), 76 | ('price_limit_min','0.00000'), 77 | ('time_start','0'), 78 | ('time_expiration','0'), 79 | ('trade_mode','4')])) 80 | logged_in = True 81 | 82 | def onSIGINT(signum, frame): 83 | global end 84 | end = True 85 | 86 | ################## 87 | # Main 88 | ################## 89 | signal.signal(signal.SIGINT, onSIGINT) 90 | m = pymt5.PyMT5(port=5533) 91 | m.onConnected = onConnected 92 | m.onDisconnected = onDisconnected 93 | m.onData = onData 94 | while not logged_in: 95 | time.sleep(1) 96 | deal = OrderedDict([('ver','3'), 97 | ('type','50'), 98 | ('exchange_id', str(int(time.time()))), 99 | ('order','1596687043'), 100 | ('symbol','XAUUSD.a'), 101 | ('login','999011'), 102 | ('type_deal','0'), 103 | ('volume','2'), 104 | ('volume_rem','0'), 105 | ('price','1355.80'), 106 | ('datetime', '1596535624555'), 107 | ('comment','TEST')]) 108 | m.send(client, deal) 109 | print('[sendExternalDeal] deal: ' + str(deal)) 110 | end = False 111 | while not end: 112 | time.sleep(1) 113 | m.stop() 114 | -------------------------------------------------------------------------------- /examples/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import pymt5 3 | import time 4 | import signal 5 | from collections import OrderedDict 6 | import json 7 | import random 8 | import string 9 | 10 | clients = [] 11 | bid = float(random.randrange(12660, 12680))/10000 12 | spread = 0.0004 13 | ask = bid+spread 14 | last = 0 15 | vol = 0 16 | 17 | def onConnected(client_info): 18 | clients.append(client_info.get('client_id')) 19 | print('[onConnected] client_address: ' + str(client_info)) 20 | print('[clients] ' + str(clients)) 21 | 22 | def onDisconnected(client_info): 23 | clients.remove(client_info.get('client_id')) 24 | print('[onDisconnected] client_address: ' + str(client_info)) 25 | print('[clients] ' + str(clients)) 26 | 27 | def sendOrderConfirmed(client, order): 28 | global last 29 | global vol 30 | order.update({'exchange_id':str(int(time.time()))}) 31 | if order['type_order'] == '0': 32 | order.update({'price_order':str(ask)}) 33 | last = ask 34 | else: 35 | order.update({'price_order':str(bid)}) 36 | last = bid 37 | vol = float(order['volume']) 38 | order.update({'state':'1'}) 39 | order.update({'result':'10009'}) 40 | m.send(client, order) 41 | m.broadcast(OrderedDict([('ver','3'),('type','4'),('symbol','EURUSD.TEST'),('bank','dc'),('bid',str(bid)),('ask',str(ask)),('last',str(last)),('volume',str(vol)),('datetime','0')])) 42 | 43 | def sendOrderPlaced(client, order): 44 | order.update({'state':'2'}) 45 | order.update({'result':'10008'}) 46 | m.send(client, order) 47 | 48 | def sendOrderNew(client, order): 49 | order.update({'state':'3'}) 50 | order.update({'result':'1'}) 51 | m.send(client, order) 52 | 53 | def sendOrderDeal(client, order): 54 | order.update({'state':'5'}) 55 | order.update({'result':'1'}) 56 | m.send(client, order) 57 | 58 | def sendOrderRejectNew(client, order): 59 | order.update({'state':'4'}) 60 | order.update({'result':'10006'}) 61 | m.send(client, order) 62 | 63 | def sendOrderCancel(client, order): 64 | order.update({'state':'10'}) 65 | order.update({'result':'10007'}) 66 | m.send(client, order) 67 | 68 | def sendOrderComplete(client, order): 69 | order.update({'state':'20'}) 70 | order.update({'result':'10009'}) 71 | m.send(client, order) 72 | 73 | def sendDeal(client, order): 74 | exchange_id = '0' 75 | while exchange_id.startswith('0'): 76 | exchange_id = ''.join(random.choice(string.digits) for i in range(8)) 77 | # type_deal: 0=buy, 1=sell 78 | deal = OrderedDict([('ver','3'), 79 | ('type','7'), 80 | ('exchange_id',exchange_id), 81 | ('order',order['order']), 82 | ('symbol',order['symbol']), 83 | ('login',order['login']), 84 | ('type_deal','0'), 85 | ('volume',order['volume']), 86 | ('volume_rem','0'), 87 | ('price','1.2679')]) 88 | m.send(client, deal) 89 | 90 | def sendDealExternal(client, exchange_id, symbol, login, type_deal, volume, price, datetime='1611339821000'): 91 | deal = OrderedDict([('ver','3'), 92 | ('type','50'), 93 | ('exchange_id',exchange_id), 94 | ('order','0'), 95 | ('symbol',symbol), 96 | ('login',login), 97 | ('type_deal',type_deal), 98 | ('volume',volume), 99 | ('volume_rem','0'), 100 | ('price',price), 101 | ('datetime',datetime)]) 102 | m.send(client, deal) 103 | 104 | def sendTick(): 105 | global bid 106 | global ask 107 | bid = float(random.randrange(12660, 12680))/10000 108 | ask = bid+spread 109 | m.broadcast(OrderedDict([('ver','3'),('type','4'),('symbol','EURUSD.TEST'),('bank','dc'),('bid',str(bid)),('ask',str(ask)),('last',str(last)),('volume',str(vol)),('datetime','0')])) 110 | 111 | def onData(data): 112 | print('[onData] data: ' + json.dumps(data)) 113 | client = data.get('client_id') 114 | # Login 115 | if data.get('type') == '1': 116 | # Send heartbeat 117 | m.send(client, OrderedDict([('ver','3'), 118 | ('type','6')])) 119 | # Send login OK response (no auth) 120 | m.send(client, OrderedDict([('ver','3'), 121 | ('type','1'), 122 | ('login',data.get('login')), 123 | ('password',data.get('password')), 124 | ('res','0')])) 125 | # Send symbol with index starting at 0 126 | time.sleep(0.5) 127 | m.send(client, OrderedDict([('ver','3'), 128 | ('type','3'), 129 | ('index','0'), 130 | ('symbol','EURUSD.TEST'), 131 | ('path','TestExchange'), 132 | ('description','test'), 133 | ('page','http://www.google.co/finance?q=EURUSD.TEST'), 134 | ('currency_base','EUR'), 135 | ('currency_profit','USD'), 136 | ('currency_margin','USD'), 137 | ('digits','4'), 138 | ('tick_flags','7'), 139 | ('calc_mode','0'), 140 | ('exec_mode','2'), 141 | ('chart_mode','0'), 142 | ('fill_flags','3'), 143 | ('expir_flags','15'), 144 | ('tick_value','0.00000'), 145 | ('tick_size','0.00000'), 146 | ('contract_size','100000.00000'), 147 | ('volume_min','0.01000'), 148 | ('volume_max','10000.00000'), 149 | ('volume_step','0.01000'), 150 | ('market_depth','0'), 151 | ('margin_flags','0'), 152 | ('margin_initial','0.00000'), 153 | ('margin_maintenance','0.00000'), 154 | ('margin_long','1.00000'), 155 | ('margin_short','1.00000'), 156 | ('margin_limit','0.00000'), 157 | ('margin_stop','0.00000'), 158 | ('margin_stop_limit','0.00000'), 159 | ('settlement_price','1.22804'), 160 | ('price_limit_max','0.00000'), 161 | ('price_limit_min','0.00000'), 162 | ('time_start','0'), 163 | ('time_expiration','0'), 164 | ('trade_mode','4')])) 165 | # Logout 166 | elif data.get('type') == '2': 167 | m.disconnect(client) 168 | # New order 169 | elif data.get('type') == '5' and data.get('state') == '0': 170 | order = data 171 | del order['client_id'] 172 | # Market 173 | if order['action'] == '2': 174 | sendOrderConfirmed(client, order) 175 | elif order['action'] == '3': 176 | sendOrderConfirmed(client, order) 177 | # Limit 178 | elif order['action'] in ('4','5'): 179 | sendOrderPlaced(client, order) 180 | sendOrderNew(client, order) 181 | sendDeal(client, order) 182 | else: 183 | sendOrderRejectNew(client, order) 184 | 185 | def onSIGINT(signum, frame): 186 | global end 187 | end = True 188 | 189 | ################## 190 | # Main 191 | ################## 192 | signal.signal(signal.SIGINT, onSIGINT) 193 | m = pymt5.PyMT5() 194 | m.onConnected = onConnected 195 | m.onDisconnected = onDisconnected 196 | m.onData = onData 197 | 198 | end = False 199 | while not end: 200 | time.sleep(1) 201 | sendTick() 202 | m.stop() 203 | -------------------------------------------------------------------------------- /pymt5/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) DevCartel Co.,Ltd. 4 | # Bangkok, Thailand 5 | # 6 | import sys 7 | __version__ = '1.4.0' 8 | 9 | if sys.version_info >= (3, 0): 10 | from pymt5.pymt5 import * 11 | else: 12 | from pymt5 import * 13 | -------------------------------------------------------------------------------- /pymt5/pymt5.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) DevCartel Co.,Ltd. 4 | # Bangkok, Thailand 5 | # 6 | import socket 7 | import threading 8 | from six.moves import socketserver 9 | import re 10 | import time 11 | from collections import OrderedDict 12 | 13 | MSG_SEPARATOR = '\n' 14 | MSG_SEPARATOR_TAG = '\x01' 15 | MSG_SEPARATOR_TAGVALUE = '=' 16 | MSG_MAX_SIZE = 4096 17 | 18 | socketserver.TCPServer.allow_reuse_address = True 19 | 20 | __all__ = ['PyMT5'] 21 | 22 | class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): 23 | def handle(self): 24 | cur_thread = threading.current_thread() 25 | requests = self.server.requests 26 | if self.request not in requests: 27 | requests[self.request] = {'client_id':cur_thread.ident, 28 | 'client_address':self.client_address[0], 29 | 'client_port':self.client_address[1]} 30 | if callable(self.server.onConnected): 31 | self.server.onConnected(requests[self.request]) 32 | buffer = b'' 33 | while True: 34 | try: 35 | part = self.request.recv(MSG_MAX_SIZE) 36 | if not part: break 37 | buffer += part 38 | if len(part) < MSG_MAX_SIZE: 39 | for decoded in buffer.decode('utf-8').split(MSG_SEPARATOR): 40 | data = OrderedDict(re.findall("(.*?)"+MSG_SEPARATOR_TAGVALUE+"(.*?)"+MSG_SEPARATOR_TAG, decoded)) 41 | if data and callable(self.server.onData): 42 | data.update({'client_id':cur_thread.ident}) 43 | self.server.onData(data) 44 | buffer = b'' 45 | except socket.error: 46 | break 47 | if callable(self.server.onDisconnected) and (self.request in requests): 48 | self.server.onDisconnected(requests[self.request]) 49 | self.request.close() 50 | 51 | class PyMT5(socketserver.ThreadingTCPServer): 52 | def __init__(self, host='', port=16838, *args, **kwargs): 53 | socketserver.ThreadingTCPServer.__init__(self, (host, port), ThreadedTCPRequestHandler) 54 | self.requests = {} 55 | self.server_thread = threading.Thread(target=self.serve_forever) 56 | self.server_thread.setDaemon(True) 57 | self.server_thread.start() 58 | self.onConnected = None 59 | self.onDisconnected = None 60 | self.onData = None 61 | 62 | def stop(self): 63 | for request in list(self.requests): 64 | self.shutdown_request(request) 65 | self.shutdown() 66 | self.server_close() 67 | 68 | def broadcast(self, data): 69 | msg = '' 70 | for k,v in data.items(): 71 | msg += k+MSG_SEPARATOR_TAGVALUE+v+MSG_SEPARATOR_TAG 72 | for request in list(self.requests): 73 | try: 74 | request.sendall((msg+MSG_SEPARATOR).encode('utf-8')) 75 | except socket.error: 76 | del self.requests[request] 77 | 78 | def send(self, client_id, data): 79 | msg = '' 80 | for k,v in data.items(): 81 | msg += k+MSG_SEPARATOR_TAGVALUE+v+MSG_SEPARATOR_TAG 82 | for request in list(self.requests): 83 | if client_id == self.requests[request]['client_id']: 84 | try: 85 | request.sendall((msg+MSG_SEPARATOR).encode('utf-8')) 86 | except socket.error: 87 | del self.requests[request] 88 | 89 | def sendRaw(self, client_id, data): 90 | for request in list(self.requests): 91 | if client_id == self.requests[request]['client_id']: 92 | try: 93 | request.sendall(data.encode('utf-8')) 94 | except socket.error: 95 | del self.requests[request] 96 | 97 | def disconnect(self, client_id): 98 | for request in list(self.requests): 99 | if client_id == self.requests[request]['client_id']: 100 | self.shutdown_request(request) 101 | del self.requests[request] 102 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE.txt 3 | 4 | [bdist_wheel] 5 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from setuptools import setup, find_packages 3 | from setuptools.dist import Distribution 4 | # To use a consistent encoding 5 | from codecs import open 6 | from os import path 7 | import platform 8 | 9 | here = path.abspath(path.dirname(__file__)) 10 | 11 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 12 | long_description = f.read() 13 | 14 | setup( 15 | name='pymt5', 16 | version='1.4.0', 17 | description='Python API for MetaTrader 5 gateways', 18 | long_description=long_description, 19 | long_description_content_type='text/markdown', 20 | url='https://github.com/devcartel/pymt5', 21 | author='DevCartel', 22 | author_email='support@devcartel.com', 23 | license='MIT', 24 | classifiers=[ 25 | 'Development Status :: 5 - Production/Stable', 26 | 'Intended Audience :: Developers', 27 | 'Topic :: Software Development :: Build Tools', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Programming Language :: Python :: 2', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.4', 33 | 'Programming Language :: Python :: 3.5', 34 | 'Programming Language :: Python :: 3.6', 35 | 'Programming Language :: Python :: 3.7', 36 | ], 37 | keywords='metatrader, metaquotes, mt5, gateway, api, python', 38 | packages=['pymt5'] 39 | ) 40 | --------------------------------------------------------------------------------