├── XTBApi ├── logs │ └── .gitkeep ├── tests │ ├── __init__.py │ ├── test_client.py │ └── test_base_client.py ├── __version__.py ├── __init__.py ├── exceptions.py └── api.py ├── MANIFEST.in ├── .gitignore ├── LICENSE ├── README.md └── setup.py /XTBApi/logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /XTBApi/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include XTBApi/logs/.gitkeep 2 | -------------------------------------------------------------------------------- /XTBApi/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "v1.0a8" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### VirtualEnv template 3 | # Virtualenv 4 | .venv 5 | # PyCharm 6 | .idea/ 7 | ### Python template 8 | # Distribution / packaging 9 | *.egg-info/ 10 | 11 | # Unit test / coverage reports 12 | .cache 13 | __pycache__/ 14 | .pytest_cache/ 15 | 16 | ### Logs 17 | *.log* 18 | 19 | ### other files 20 | .DS_Store 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Federico Lolli 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 | -------------------------------------------------------------------------------- /XTBApi/__init__.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | import os.path 3 | 4 | from XTBApi.__version__ import __version__ 5 | 6 | logging.config.dictConfig({ 7 | 'version': 1, 8 | 'disable_existing_loggers': False, 9 | 'formatters': { 10 | 'deafult': { 11 | 'format': 12 | '%(asctime)s - %(levelname)s - %(name)s - %(message)s', 13 | 'datefmt': '%Y-%m-%d %H:%M:%S' 14 | } 15 | }, 16 | 'handlers': { 17 | 'console': { 18 | 'class': 'logging.StreamHandler', 19 | 'formatter': 'deafult', 20 | }, 21 | 'rotating': { 22 | 'class': 'logging.handlers.TimedRotatingFileHandler', 23 | 'formatter': 'deafult', 24 | 'filename': os.path.join( 25 | os.path.dirname(__file__), 'logs/logfile.log'), 26 | 'when': 'midnight', 27 | 'backupCount': 3 28 | } 29 | }, 30 | 'loggers': { 31 | '': { 32 | 'handlers': ['console'], 33 | 'level': 'CRITICAL', 34 | 'propagate': True 35 | }, 36 | 'XTBApi': { 37 | 'handlers': ['rotating'], 38 | 'level': 'DEBUG' 39 | } 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /XTBApi/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding utf-8 -*- 2 | 3 | """ 4 | XTBApi.exceptions 5 | ~~~~~~~ 6 | 7 | Exception module 8 | """ 9 | 10 | import logging 11 | 12 | LOGGER = logging.getLogger('XTBApi.exceptions') 13 | 14 | 15 | class CommandFailed(Exception): 16 | """when a command fail""" 17 | def __init__(self, response): 18 | self.msg = "command failed" 19 | self.err_code = response['errorCode'] 20 | super().__init__(self.msg) 21 | 22 | 23 | class NotLogged(Exception): 24 | """when not logged""" 25 | def __init__(self): 26 | self.msg = "Not logged, please log in" 27 | LOGGER.exception(self.msg) 28 | super().__init__(self.msg) 29 | 30 | 31 | class SocketError(Exception): 32 | """when socket is already closed 33 | may be the case of server internal error""" 34 | def __init__(self): 35 | self.msg = "SocketError, mey be an internal error" 36 | LOGGER.error(self.msg) 37 | super().__init__(self.msg) 38 | 39 | 40 | class TransactionRejected(Exception): 41 | """transaction rejected error""" 42 | def __init__(self, status_code): 43 | self.status_code = status_code 44 | self.msg = "transaction rejected with error code {}".format( 45 | status_code) 46 | LOGGER.error(self.msg) 47 | super().__init__(self.msg) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XTBApi 2 | 3 | > Api for XTB trading platform. 4 | 5 | A python based API for XTB trading using _websocket_client_. 6 | 7 | # Installing / Getting started 8 | 9 | To install the API, just clone the repository. 10 | 11 | ```bash 12 | git clone git@github.com:federico123579/XTBApi.git 13 | cd XTBApi/ 14 | python3 -m venv env 15 | . env/bin/activate 16 | pip install . 17 | ``` 18 | 19 | Then you can use XTBApi like this simple tutorial. 20 | ```python 21 | from XTBApi.api import Client 22 | # FIRST INIT THE CLIENT 23 | client = Client() 24 | # THEN LOGIN 25 | client.login("{user_id}", "{password}", mode={demo,real}) 26 | # CHECK IF MARKET IS OPEN FOR EURUSD 27 | client.check_if_market_open([EURUSD]) 28 | # BUY ONE VOLUME (FOR EURUSD THAT CORRESPONDS TO 100000 units) 29 | client.open_trade('buy', EURUSD, 1) 30 | # SEE IF ACTUAL GAIN IS ABOVE 100 THEN CLOSE THE TRADE 31 | trades = client.update_trades() # GET CURRENT TRADES 32 | trade_ids = [trade_id for trade_id in trades.keys()] 33 | for trade in trade_ids: 34 | actual_profit = client.get_trade_profit(trade) # CHECK PROFIT 35 | if actual_profit >= 100: 36 | client.close_trade(trade) # CLOSE TRADE 37 | # CLOSE ALL OPEN TRADES 38 | client.close_all_trades() 39 | # THEN LOGOUT 40 | client.logout() 41 | ``` 42 | 43 | # Api Reference 44 | REQUIRED - **SOON** 45 | 46 | _Documentation still in progess_ 47 | -------------------------------------------------------------------------------- /XTBApi/tests/test_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | tests.test_client.py 3 | ~~~~~~~ 4 | 5 | test the api client 6 | """ 7 | 8 | import logging 9 | 10 | import pytest 11 | 12 | from XTBApi.api import Client 13 | 14 | LOGGER = logging.getLogger('XTBApi.test_client') 15 | logging.getLogger('XTBApi.api').setLevel(logging.INFO) 16 | 17 | USERID = "" #REMOVE 18 | PASSWORD = "" #REMOVED 19 | DEFAULT_CURRENCY = 'EURUSD' 20 | 21 | 22 | @pytest.fixture(scope="module") 23 | def _get_client(): 24 | return Client() 25 | 26 | 27 | def test_login(_get_client): 28 | client = _get_client 29 | client.login(USERID, PASSWORD) 30 | LOGGER.debug("passed") 31 | 32 | 33 | def test_trades(_get_client): 34 | client = _get_client 35 | trades = client.update_trades() 36 | LOGGER.debug(trades) 37 | LOGGER.debug("passed") 38 | 39 | 40 | class TestMarket: 41 | @staticmethod 42 | def test_trade_open(_get_client): 43 | client = _get_client 44 | trade_id = client.open_trade(0, DEFAULT_CURRENCY, 0.1) 45 | LOGGER.debug(trade_id) 46 | LOGGER.debug("passed") 47 | 48 | @staticmethod 49 | def test_profit(_get_client): 50 | client = _get_client 51 | trade_id = client.get_trades()[0]['order'] 52 | trade_profit = client.get_trade_profit(trade_id) 53 | LOGGER.debug(trade_profit) 54 | LOGGER.debug("passed") 55 | 56 | @staticmethod 57 | def test_close_trade(_get_client): 58 | client = _get_client 59 | trade_id = client.get_trades()[0]['order'] 60 | client.close_trade(trade_id) 61 | LOGGER.debug("passed") 62 | 63 | @staticmethod 64 | def test_close_all(_get_client): 65 | client = _get_client 66 | client.close_all_trades() 67 | LOGGER.debug("passed") 68 | 69 | 70 | # at the end of file 71 | def test_logout(_get_client): 72 | client = _get_client 73 | client.logout() 74 | LOGGER.debug("passed") 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pip install twine 6 | 7 | import io 8 | import os 9 | import sys 10 | from shutil import rmtree 11 | 12 | from setuptools import find_packages, setup, Command 13 | 14 | # Package meta-data. 15 | NAME = 'XTBApi' 16 | DESCRIPTION = 'Api for XBT trading platform' 17 | URL = 'https://github.com/federico123579/XTBApi.git' 18 | EMAIL = 'federico123579@gmail.com' 19 | AUTHOR = 'Federico Lolli' 20 | REQUIRES_PYTHON = '>=3.6.0' 21 | VERSION = None 22 | 23 | # What packages are required for this module to be executed? 24 | REQUIRED = [ 25 | 'websocket_client' 26 | ] 27 | 28 | # What packages are optional? 29 | EXTRAS = { 30 | 'test': ['pytest'] 31 | # 'fancy feature': ['django'], 32 | } 33 | 34 | here = os.path.abspath(os.path.dirname(__file__)) 35 | 36 | # Import the README and use it as the long-description. 37 | # Note: this will only work if 'README.md' is present in your MANIFEST.in file! 38 | try: 39 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 40 | long_description = '\n' + f.read() 41 | except FileNotFoundError: 42 | long_description = DESCRIPTION 43 | 44 | # Load the package's __version__.py module as a dictionary. 45 | about = {} 46 | if not VERSION: 47 | with open(os.path.join(here, NAME, '__version__.py')) as f: 48 | exec(f.read(), about) 49 | else: 50 | about['__version__'] = VERSION 51 | 52 | 53 | class UploadCommand(Command): 54 | """Support setup.py upload.""" 55 | 56 | description = 'Build and publish the package.' 57 | user_options = [] 58 | 59 | @staticmethod 60 | def status(s): 61 | """Prints things in bold.""" 62 | print('\033[1m{0}\033[0m'.format(s)) 63 | 64 | def initialize_options(self): 65 | pass 66 | 67 | def finalize_options(self): 68 | pass 69 | 70 | def run(self): 71 | try: 72 | self.status('Removing previous builds…') 73 | rmtree(os.path.join(here, 'dist')) 74 | except OSError: 75 | pass 76 | 77 | self.status('Building Source and Wheel (universal) distribution…') 78 | os.system( 79 | '{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 80 | 81 | self.status('Uploading the package to PyPI via Twine…') 82 | os.system('twine upload dist/*') 83 | 84 | self.status('Pushing git tags…') 85 | os.system('git tag {0}'.format(about['__version__'])) 86 | os.system('git push --tags') 87 | 88 | sys.exit() 89 | 90 | 91 | # Where the magic happens: 92 | setup( 93 | name=NAME, 94 | version=about['__version__'], 95 | description=DESCRIPTION, 96 | long_description=long_description, 97 | long_description_content_type='text/markdown', 98 | author=AUTHOR, 99 | author_email=EMAIL, 100 | python_requires=REQUIRES_PYTHON, 101 | url=URL, 102 | packages=find_packages(exclude=('tests',)), 103 | package_data={ 104 | "": ["logs/.gitkeep"], 105 | }, 106 | # If your package is a single module, use this instead of 'packages': 107 | # py_modules=['mypackage'], 108 | 109 | # entry_points={ 110 | # 'console_scripts': ['mycli=mymodule:cli'], 111 | # }, 112 | install_requires=REQUIRED, 113 | extras_require=EXTRAS, 114 | include_package_data=True, 115 | license='MIT', 116 | classifiers=[ 117 | # Trove classifiers 118 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 119 | 'License :: OSI Approved :: MIT License', 120 | 'Programming Language :: Python', 121 | 'Programming Language :: Python :: 3', 122 | 'Programming Language :: Python :: 3.6', 123 | 'Programming Language :: Python :: Implementation :: CPython', 124 | 'Programming Language :: Python :: Implementation :: PyPy' 125 | ], 126 | # $ setup.py publish support. 127 | cmdclass={ 128 | 'upload': UploadCommand, 129 | }, 130 | ) 131 | -------------------------------------------------------------------------------- /XTBApi/tests/test_base_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | tests.test_base_client.py 3 | ~~~~~~~ 4 | 5 | test the api client 6 | """ 7 | 8 | import logging 9 | import time 10 | 11 | import pytest 12 | 13 | import XTBApi.api 14 | 15 | LOGGER = logging.getLogger('XTBApi.test_base_client') 16 | logging.getLogger('XTBApi.api').setLevel(logging.DEBUG) 17 | 18 | USER_ID = '' #REMOVED 19 | PASSWORD = '' #REMOVED 20 | DEFAULT_CURRENCY = 'EURUSD' 21 | 22 | 23 | @pytest.fixture(scope="module") 24 | def _get_client(): 25 | return XTBApi.api.BaseClient() 26 | 27 | 28 | def test_login(_get_client): 29 | client = _get_client 30 | client.login(USER_ID, PASSWORD) 31 | LOGGER.debug("passed") 32 | 33 | 34 | def test_all_symbols(_get_client): 35 | client = _get_client 36 | client.get_all_symbols() 37 | LOGGER.debug("passed") 38 | 39 | 40 | def test_get_calendar(_get_client): 41 | client = _get_client 42 | client.get_calendar() 43 | LOGGER.debug("passed") 44 | 45 | 46 | def test_get_chart_last_request(_get_client): 47 | client = _get_client 48 | start = time.time() - 3600 * 24 49 | args = [DEFAULT_CURRENCY, 1440, start] 50 | client.get_chart_last_request(*args) 51 | LOGGER.debug("passed") 52 | 53 | 54 | def test_get_chart_range_request(_get_client): 55 | client = _get_client 56 | start = (time.time() - 3600 * 24 * 2) 57 | end = (time.time() - 3600 * 24) 58 | args = [DEFAULT_CURRENCY, 1440, start, end, 0] 59 | client.get_chart_range_request(*args) 60 | LOGGER.debug("passed") 61 | 62 | 63 | def test_get_commission(_get_client): 64 | client = _get_client 65 | client.get_commission(DEFAULT_CURRENCY, 1.0) 66 | LOGGER.debug("passed") 67 | 68 | 69 | def test_get_user_data(_get_client): 70 | client = _get_client 71 | client.get_user_data() 72 | LOGGER.debug("passed") 73 | 74 | 75 | def test_get_margin_level(_get_client): 76 | client = _get_client 77 | client.get_margin_level() 78 | LOGGER.debug("passed") 79 | 80 | 81 | def test_get_margin_trade(_get_client): 82 | client = _get_client 83 | client.get_margin_trade(DEFAULT_CURRENCY, 1.0) 84 | LOGGER.debug("passed") 85 | 86 | 87 | def test_get_profit_calculation(_get_client): 88 | client = _get_client 89 | args = [DEFAULT_CURRENCY, 0, 1.0, 1.2233, 1.3000] 90 | client.get_profit_calculation(*args) 91 | LOGGER.debug("passed") 92 | 93 | 94 | def test_get_server_time(_get_client): 95 | client = _get_client 96 | client.get_server_time() 97 | LOGGER.debug("passed") 98 | 99 | 100 | def test_symbol(_get_client): 101 | client = _get_client 102 | client.get_symbol(DEFAULT_CURRENCY) 103 | LOGGER.debug("passed") 104 | 105 | 106 | def test_get_tick_prices(_get_client): 107 | client = _get_client 108 | args = [[DEFAULT_CURRENCY], time.time() - 3600 * 24, 0] 109 | client.get_tick_prices(*args) 110 | LOGGER.debug("passed") 111 | 112 | 113 | def test_get_trade_records(_get_client): 114 | client = _get_client 115 | client.get_trade_records([7489839]) 116 | LOGGER.debug("passed") 117 | 118 | 119 | def test_get_trades(_get_client): 120 | client = _get_client 121 | client.get_trades(True) 122 | LOGGER.debug("passed") 123 | 124 | 125 | def test_get_trades_history(_get_client): 126 | client = _get_client 127 | args = [time.time() - 3600 * 24, 0] 128 | client.get_trades_history(*args) 129 | LOGGER.debug("passed") 130 | 131 | 132 | def test_get_trading_hours(_get_client): 133 | client = _get_client 134 | client.get_trading_hours([DEFAULT_CURRENCY]) 135 | LOGGER.debug("passed") 136 | 137 | 138 | def test_get_version(_get_client): 139 | client = _get_client 140 | client.get_version() 141 | LOGGER.debug("passed") 142 | 143 | 144 | def test_ping(_get_client): 145 | client = _get_client 146 | client.ping() 147 | LOGGER.debug("passed") 148 | 149 | 150 | def test_trade_transaction(_get_client): 151 | client = _get_client 152 | price = client.get_symbol(DEFAULT_CURRENCY)['ask'] 153 | args = [DEFAULT_CURRENCY, 0, 0, 5.0] 154 | client.trade_transaction(*args, price=price) 155 | LOGGER.debug("passed") 156 | 157 | 158 | def test_trade_transaction_status(_get_client: object): 159 | client = _get_client 160 | price = client.get_symbol(DEFAULT_CURRENCY)['ask'] 161 | args = [DEFAULT_CURRENCY, 0, 0, 5.0] 162 | pos_id = client.trade_transaction(*args, price=price)['order'] 163 | client.trade_transaction_status(pos_id) 164 | LOGGER.debug("passed") 165 | 166 | 167 | # at the end of file 168 | def test_logout(_get_client): 169 | client = _get_client 170 | client.logout() 171 | LOGGER.debug("passed") 172 | -------------------------------------------------------------------------------- /XTBApi/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding utf-8 -*- 2 | 3 | """ 4 | XTBApi.api 5 | ~~~~~~~ 6 | 7 | Main module 8 | """ 9 | 10 | import enum 11 | import inspect 12 | import json 13 | import logging 14 | import time 15 | from datetime import datetime 16 | 17 | from websocket import create_connection 18 | from websocket._exceptions import WebSocketConnectionClosedException 19 | 20 | from XTBApi.exceptions import * 21 | 22 | LOGGER = logging.getLogger('XTBApi.api') 23 | LOGIN_TIMEOUT = 120 24 | MAX_TIME_INTERVAL = 0.200 25 | 26 | 27 | class STATUS(enum.Enum): 28 | LOGGED = enum.auto() 29 | NOT_LOGGED = enum.auto() 30 | 31 | 32 | class MODES(enum.Enum): 33 | BUY = 0 34 | SELL = 1 35 | BUY_LIMIT = 2 36 | SELL_LIMIT = 3 37 | BUY_STOP = 4 38 | SELL_STOP = 5 39 | BALANCE = 6 40 | CREDIT = 7 41 | 42 | 43 | class TRANS_TYPES(enum.Enum): 44 | OPEN = 0 45 | PENDING = 1 46 | CLOSE = 2 47 | MODIFY = 3 48 | DELETE = 4 49 | 50 | 51 | class PERIOD(enum.Enum): 52 | ONE_MINUTE = 1 53 | FIVE_MINUTES = 5 54 | FIFTEEN_MINUTES = 15 55 | THIRTY_MINUTES = 30 56 | ONE_HOUR = 60 57 | FOUR_HOURS = 240 58 | ONE_DAY = 1440 59 | ONE_WEEK = 10080 60 | ONE_MONTH = 43200 61 | 62 | 63 | def _get_data(command, **parameters): 64 | data = { 65 | "command": command, 66 | } 67 | if parameters: 68 | data['arguments'] = {} 69 | for (key, value) in parameters.items(): 70 | data['arguments'][key] = value 71 | return data 72 | 73 | 74 | def _check_mode(mode): 75 | """check if mode acceptable""" 76 | modes = [x.value for x in MODES] 77 | if mode not in modes: 78 | raise ValueError("mode must be in {}".format(modes)) 79 | 80 | 81 | def _check_period(period): 82 | """check if period is acceptable""" 83 | if period not in [x.value for x in PERIOD]: 84 | raise ValueError("Period: {} not acceptable".format(period)) 85 | 86 | 87 | def _check_volume(volume): 88 | """normalize volume""" 89 | if not isinstance(volume, float): 90 | try: 91 | return float(volume) 92 | except Exception: 93 | raise ValueError("vol must be float") 94 | else: 95 | return volume 96 | 97 | 98 | class BaseClient(object): 99 | """main client class""" 100 | 101 | def __init__(self): 102 | self.ws = None 103 | self._login_data = None 104 | self._time_last_request = time.time() - MAX_TIME_INTERVAL 105 | self.status = STATUS.NOT_LOGGED 106 | LOGGER.debug("BaseClient inited") 107 | self.LOGGER = logging.getLogger('XTBApi.api.BaseClient') 108 | 109 | def _login_decorator(self, func, *args, **kwargs): 110 | if self.status == STATUS.NOT_LOGGED: 111 | raise NotLogged() 112 | try: 113 | return func(*args, **kwargs) 114 | except SocketError as e: 115 | LOGGER.info("re-logging in due to LOGIN_TIMEOUT gone") 116 | self.login(self._login_data[0], self._login_data[1]) 117 | return func(*args, **kwargs) 118 | except Exception as e: 119 | LOGGER.warning(e) 120 | self.login(self._login_data[0], self._login_data[1]) 121 | return func(*args, **kwargs) 122 | 123 | def _send_command(self, dict_data): 124 | """send command to api""" 125 | time_interval = time.time() - self._time_last_request 126 | self.LOGGER.debug("took {} s.".format(time_interval)) 127 | if time_interval < MAX_TIME_INTERVAL: 128 | time.sleep(MAX_TIME_INTERVAL - time_interval) 129 | try: 130 | self.ws.send(json.dumps(dict_data)) 131 | response = self.ws.recv() 132 | except WebSocketConnectionClosedException: 133 | raise SocketError() 134 | self._time_last_request = time.time() 135 | res = json.loads(response) 136 | if res['status'] is False: 137 | raise CommandFailed(res) 138 | if 'returnData' in res.keys(): 139 | self.LOGGER.info("CMD: done") 140 | self.LOGGER.debug(res['returnData']) 141 | return res['returnData'] 142 | 143 | def _send_command_with_check(self, dict_data): 144 | """with check login""" 145 | return self._login_decorator(self._send_command, dict_data) 146 | 147 | def login(self, user_id, password, mode='demo'): 148 | """login command""" 149 | data = _get_data("login", userId=user_id, password=password) 150 | self.ws = create_connection(f"wss://ws.xtb.com/{mode}") 151 | response = self._send_command(data) 152 | self._login_data = (user_id, password) 153 | self.status = STATUS.LOGGED 154 | self.LOGGER.info("CMD: login...") 155 | return response 156 | 157 | def logout(self): 158 | """logout command""" 159 | data = _get_data("logout") 160 | response = self._send_command(data) 161 | self.status = STATUS.LOGGED 162 | self.LOGGER.info("CMD: logout...") 163 | return response 164 | 165 | def get_all_symbols(self): 166 | """getAllSymbols command""" 167 | data = _get_data("getAllSymbols") 168 | self.LOGGER.info("CMD: get all symbols...") 169 | return self._send_command_with_check(data) 170 | 171 | def get_calendar(self): 172 | """getCalendar command""" 173 | data = _get_data("getCalendar") 174 | self.LOGGER.info("CMD: get calendar...") 175 | return self._send_command_with_check(data) 176 | 177 | def get_chart_last_request(self, symbol, period, start): 178 | """getChartLastRequest command""" 179 | _check_period(period) 180 | args = { 181 | "period": period, 182 | "start": start * 1000, 183 | "symbol": symbol 184 | } 185 | data = _get_data("getChartLastRequest", info=args) 186 | self.LOGGER.info(f"CMD: get chart last request for {symbol} of period" 187 | f" {period} from {start}...") 188 | 189 | return self._send_command_with_check(data) 190 | 191 | def get_chart_range_request(self, symbol, period, start, end, ticks): 192 | """getChartRangeRequest command""" 193 | if not isinstance(ticks, int): 194 | raise ValueError(f"ticks value {ticks} must be int") 195 | self._check_login() 196 | args = { 197 | "end": end * 1000, 198 | "period": period, 199 | "start": start * 1000, 200 | "symbol": symbol, 201 | "ticks": ticks 202 | } 203 | data = _get_data("getChartRangeRequest", info=args) 204 | self.LOGGER.info(f"CMD: get chart range request for {symbol} of " 205 | f"{period} from {start} to {end} with ticks of " 206 | f"{ticks}...") 207 | return self._send_command_with_check(data) 208 | 209 | def get_commission(self, symbol, volume): 210 | """getCommissionDef command""" 211 | volume = _check_volume(volume) 212 | data = _get_data("getCommissionDef", symbol=symbol, volume=volume) 213 | self.LOGGER.info(f"CMD: get commission for {symbol} of {volume}...") 214 | return self._send_command_with_check(data) 215 | 216 | def get_margin_level(self): 217 | """getMarginLevel command 218 | get margin information""" 219 | data = _get_data("getMarginLevel") 220 | self.LOGGER.info("CMD: get margin level...") 221 | return self._send_command_with_check(data) 222 | 223 | def get_margin_trade(self, symbol, volume): 224 | """getMarginTrade command 225 | get expected margin for volumes used symbol""" 226 | volume = _check_volume(volume) 227 | data = _get_data("getMarginTrade", symbol=symbol, volume=volume) 228 | self.LOGGER.info(f"CMD: get margin trade for {symbol} of {volume}...") 229 | return self._send_command_with_check(data) 230 | 231 | def get_profit_calculation(self, symbol, mode, volume, op_price, cl_price): 232 | """getProfitCalculation command 233 | get profit calculation for symbol with vol, mode and op, cl prices""" 234 | _check_mode(mode) 235 | volume = _check_volume(volume) 236 | data = _get_data("getProfitCalculation", closePrice=cl_price, 237 | cmd=mode, openPrice=op_price, symbol=symbol, 238 | volume=volume) 239 | self.LOGGER.info(f"CMD: get profit calculation for {symbol} of " 240 | f"{volume} from {op_price} to {cl_price} in mode " 241 | f"{mode}...") 242 | return self._send_command_with_check(data) 243 | 244 | def get_server_time(self): 245 | """getServerTime command""" 246 | data = _get_data("getServerTime") 247 | self.LOGGER.info("CMD: get server time...") 248 | return self._send_command_with_check(data) 249 | 250 | def get_symbol(self, symbol): 251 | """getSymbol command""" 252 | data = _get_data("getSymbol", symbol=symbol) 253 | self.LOGGER.info(f"CMD: get symbol {symbol}...") 254 | return self._send_command_with_check(data) 255 | 256 | def get_tick_prices(self, symbols, start, level=0): 257 | """getTickPrices command""" 258 | data = _get_data("getTickPrices", level=level, symbols=symbols, 259 | timestamp=start) 260 | self.LOGGER.info(f"CMD: get tick prices of {symbols} from {start} " 261 | f"with level {level}...") 262 | return self._send_command_with_check(data) 263 | 264 | def get_trade_records(self, trade_position_list): 265 | """getTradeRecords command 266 | takes a list of position id""" 267 | data = _get_data("getTradeRecords", orders=trade_position_list) 268 | self.LOGGER.info(f"CMD: get trade records of len " 269 | f"{len(trade_position_list)}...") 270 | return self._send_command_with_check(data) 271 | 272 | def get_trades(self, opened_only=True): 273 | """getTrades command""" 274 | data = _get_data("getTrades", openedOnly=opened_only) 275 | self.LOGGER.info("CMD: get trades...") 276 | return self._send_command_with_check(data) 277 | 278 | def get_trades_history(self, start, end): 279 | """getTradesHistory command 280 | can take 0 as actual time""" 281 | data = _get_data("getTradesHistory", end=end, start=start) 282 | self.LOGGER.info(f"CMD: get trades history from {start} to {end}...") 283 | return self._send_command_with_check(data) 284 | 285 | def get_trading_hours(self, trade_position_list): 286 | """getTradingHours command""" 287 | # EDITED IN ALPHA2 288 | data = _get_data("getTradingHours", symbols=trade_position_list) 289 | self.LOGGER.info(f"CMD: get trading hours of len " 290 | f"{len(trade_position_list)}...") 291 | response = self._send_command_with_check(data) 292 | for symbol in response: 293 | for day in symbol['trading']: 294 | day['fromT'] = int(day['fromT'] / 1000) 295 | day['toT'] = int(day['toT'] / 1000) 296 | for day in symbol['quotes']: 297 | day['fromT'] = int(day['fromT'] / 1000) 298 | day['toT'] = int(day['toT'] / 1000) 299 | return response 300 | 301 | def get_version(self): 302 | """getVersion command""" 303 | data = _get_data("getVersion") 304 | self.LOGGER.info("CMD: get version...") 305 | return self._send_command_with_check(data) 306 | 307 | def ping(self): 308 | """ping command""" 309 | data = _get_data("ping") 310 | self.LOGGER.info("CMD: get ping...") 311 | self._send_command_with_check(data) 312 | 313 | def trade_transaction(self, symbol, mode, trans_type, volume, stop_loss=0, 314 | take_profit=0, **kwargs): 315 | """tradeTransaction command""" 316 | # check type 317 | if trans_type not in [x.value for x in TRANS_TYPES]: 318 | raise ValueError(f"Type must be in {[x for x in trans_type]}") 319 | # check sl & tp 320 | stop_loss = float(stop_loss) 321 | take_profit = float(take_profit) 322 | # check kwargs 323 | accepted_values = ['order', 'price', 'expiration', 'customComment', 324 | 'offset', 'sl', 'tp'] 325 | assert all([val in accepted_values for val in kwargs.keys()]) 326 | _check_mode(mode) # check if mode is acceptable 327 | volume = _check_volume(volume) # check if volume is valid 328 | info = { 329 | 'cmd': mode, 330 | 'symbol': symbol, 331 | 'type': trans_type, 332 | 'volume': volume, 333 | 'sl': stop_loss, 334 | 'tp': take_profit 335 | } 336 | info.update(kwargs) # update with kwargs parameters 337 | data = _get_data("tradeTransaction", tradeTransInfo=info) 338 | name_of_mode = [x.name for x in MODES if x.value == mode][0] 339 | name_of_type = [x.name for x in TRANS_TYPES if x.value == 340 | trans_type][0] 341 | self.LOGGER.info(f"CMD: trade transaction of {symbol} of mode " 342 | f"{name_of_mode} with type {name_of_type} of " 343 | f"{volume}...") 344 | return self._send_command_with_check(data) 345 | 346 | def trade_transaction_status(self, order_id): 347 | """tradeTransactionStatus command""" 348 | data = _get_data("tradeTransactionStatus", order=order_id) 349 | self.LOGGER.info(f"CMD: trade transaction status for {order_id}...") 350 | return self._send_command_with_check(data) 351 | 352 | def get_user_data(self): 353 | """getCurrentUserData command""" 354 | data = _get_data("getCurrentUserData") 355 | self.LOGGER.info("CMD: get user data...") 356 | return self._send_command_with_check(data) 357 | 358 | 359 | class Transaction(object): 360 | def __init__(self, trans_dict): 361 | self._trans_dict = trans_dict 362 | self.mode = {0: 'buy', 1: 'sell'}[trans_dict['cmd']] 363 | self.order_id = trans_dict['order'] 364 | self.symbol = trans_dict['symbol'] 365 | self.volume = trans_dict['volume'] 366 | self.price = trans_dict['close_price'] 367 | self.actual_profit = trans_dict['profit'] 368 | self.timestamp = trans_dict['open_time'] / 1000 369 | LOGGER.debug(f"Transaction {self.order_id} inited") 370 | 371 | 372 | class Client(BaseClient): 373 | """advanced class of client""" 374 | def __init__(self): 375 | super().__init__() 376 | self.trade_rec = {} 377 | self.LOGGER = logging.getLogger('XTBApi.api.Client') 378 | self.LOGGER.info("Client inited") 379 | 380 | def check_if_market_open(self, list_of_symbols): 381 | """check if market is open for symbol in symbols""" 382 | _td = datetime.today() 383 | actual_tmsp = _td.hour * 3600 + _td.minute * 60 + _td.second 384 | response = self.get_trading_hours(list_of_symbols) 385 | market_values = {} 386 | for symbol in response: 387 | today_values = [day for day in symbol['trading'] if day['day'] == 388 | _td.isoweekday()][0] 389 | if today_values['fromT'] <= actual_tmsp <= today_values['toT']: 390 | market_values[symbol['symbol']] = True 391 | else: 392 | market_values[symbol['symbol']] = False 393 | return market_values 394 | 395 | def get_lastn_candle_history(self, symbol, timeframe_in_seconds, number): 396 | """get last n candles of timeframe""" 397 | acc_tmf = [60, 300, 900, 1800, 3600, 14400, 86400, 604800, 2592000] 398 | if timeframe_in_seconds not in acc_tmf: 399 | raise ValueError(f"timeframe not accepted, not in " 400 | f"{', '.join([str(x) for x in acc_tmf])}") 401 | sec_prior = timeframe_in_seconds * number 402 | LOGGER.debug(f"sym: {symbol}, tmf: {timeframe_in_seconds}," 403 | f" {time.time() - sec_prior}") 404 | res = {'rateInfos': []} 405 | while len(res['rateInfos']) < number: 406 | res = self.get_chart_last_request(symbol, 407 | timeframe_in_seconds // 60, time.time() - sec_prior) 408 | LOGGER.debug(res) 409 | res['rateInfos'] = res['rateInfos'][-number:] 410 | sec_prior *= 3 411 | candle_history = [] 412 | for candle in res['rateInfos']: 413 | _pr = candle['open'] 414 | op_pr = _pr / 10 ** res['digits'] 415 | cl_pr = (_pr + candle['close']) / 10 ** res['digits'] 416 | hg_pr = (_pr + candle['high']) / 10 ** res['digits'] 417 | lw_pr = (_pr + candle['low']) / 10 ** res['digits'] 418 | new_candle_entry = {'timestamp': candle['ctm'] / 1000, 'open': 419 | op_pr, 'close': cl_pr, 'high': hg_pr, 'low': lw_pr, 420 | 'volume': candle['vol']} 421 | candle_history.append(new_candle_entry) 422 | LOGGER.debug(candle_history) 423 | return candle_history 424 | 425 | def update_trades(self): 426 | """update trade list""" 427 | trades = self.get_trades() 428 | self.trade_rec.clear() 429 | for trade in trades: 430 | obj_trans = Transaction(trade) 431 | self.trade_rec[obj_trans.order_id] = obj_trans 432 | #values_to_del = [key for key, trad_not_listed in 433 | # self.trade_rec.items() if trad_not_listed.order_id 434 | # not in [x['order'] for x in trades]] 435 | #for key in values_to_del: 436 | # del self.trade_rec[key] 437 | self.LOGGER.info(f"updated {len(self.trade_rec)} trades") 438 | return self.trade_rec 439 | 440 | def get_trade_profit(self, trans_id): 441 | """get profit of trade""" 442 | self.update_trades() 443 | profit = self.trade_rec[trans_id].actual_profit 444 | self.LOGGER.info(f"got trade profit of {profit}") 445 | return profit 446 | 447 | def open_trade(self, mode, symbol, volume): 448 | """open trade transaction""" 449 | if mode in [MODES.BUY.value, MODES.SELL.value]: 450 | mode = [x for x in MODES if x.value == mode][0] 451 | elif mode in ['buy', 'sell']: 452 | modes = {'buy': MODES.BUY, 'sell': MODES.SELL} 453 | mode = modes[mode] 454 | else: 455 | raise ValueError("mode can be buy or sell") 456 | mode_name = mode.name 457 | mode = mode.value 458 | self.LOGGER.debug(f"opening trade of {symbol} of {volume} with " 459 | f"{mode_name}") 460 | conversion_mode = {MODES.BUY.value: 'ask', MODES.SELL.value: 'bid'} 461 | price = self.get_symbol(symbol)[conversion_mode[mode]] 462 | response = self.trade_transaction(symbol, mode, 0, volume, price=price) 463 | self.update_trades() 464 | status = self.trade_transaction_status(response['order'])[ 465 | 'requestStatus'] 466 | self.LOGGER.debug(f"open_trade completed with status of {status}") 467 | if status != 3: 468 | raise TransactionRejected(status) 469 | return response 470 | 471 | def _close_trade_only(self, order_id): 472 | """faster but less secure""" 473 | trade = self.trade_rec[order_id] 474 | self.LOGGER.debug(f"closing trade {order_id}") 475 | try: 476 | response = self.trade_transaction( 477 | trade.symbol, 0, 2, trade.volume, order=trade.order_id, 478 | price=trade.price) 479 | except CommandFailed as e: 480 | if e.err_code == 'BE51': # order already closed 481 | self.LOGGER.debug("BE51 error code noticed") 482 | return 'BE51' 483 | else: 484 | raise 485 | status = self.trade_transaction_status( 486 | response['order'])['requestStatus'] 487 | self.LOGGER.debug(f"close_trade completed with status of {status}") 488 | if status != 3: 489 | raise TransactionRejected(status) 490 | return response 491 | 492 | def close_trade(self, trans): 493 | """close trade transaction""" 494 | if isinstance(trans, Transaction): 495 | order_id = trans.order_id 496 | else: 497 | order_id = trans 498 | self.update_trades() 499 | return self._close_trade_only(order_id) 500 | 501 | def close_all_trades(self): 502 | """close all trades""" 503 | self.update_trades() 504 | self.LOGGER.debug(f"closing {len(self.trade_rec)} trades") 505 | trade_ids = self.trade_rec.keys() 506 | for trade_id in trade_ids: 507 | self._close_trade_only(trade_id) 508 | 509 | 510 | # - next features - 511 | # TODO: withdraw 512 | # TODO: deposit 513 | --------------------------------------------------------------------------------