├── .gitignore ├── RestApiNotebook.ipynb ├── fxcm_rest.json ├── fxcm_rest_api.py ├── fxcm_rest_api_token.py ├── fxcm_rest_client_sample.py ├── readme.md └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | @ -1,94 +0,0 @@ 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | .gitignore 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 31 | node_modules 32 | ### Python template 33 | # Byte-compiled / optimized / DLL files 34 | __pycache__/ 35 | *.py[cod] 36 | *$py.class 37 | 38 | # C extensions 39 | *.so 40 | 41 | # Distribution / packaging 42 | .Python 43 | env/ 44 | build/ 45 | develop-eggs/ 46 | dist/ 47 | downloads/ 48 | eggs/ 49 | .eggs/ 50 | lib/ 51 | lib64/ 52 | parts/ 53 | sdist/ 54 | var/ 55 | *.egg-info/ 56 | .installed.cfg 57 | *.egg 58 | 59 | # PyInstaller 60 | # Usually these files are written by a python script from a template 61 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 62 | *.manifest 63 | *.spec 64 | 65 | # Installer logs 66 | pip-log.txt 67 | pip-delete-this-directory.txt 68 | 69 | # Unit test / coverage reports 70 | htmlcov/ 71 | .tox/ 72 | .coverage 73 | .coverage.* 74 | .cache 75 | nosetests.xml 76 | coverage.xml 77 | *,cover 78 | 79 | # Translations 80 | *.mo 81 | *.pot 82 | 83 | # Django stuff: 84 | *.log 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | 89 | # PyBuilder 90 | target/ 91 | 92 | # Created by .ignore support plugin (hsz.mobi) 93 | 94 | .idea/ 95 | crossbar/ 96 | fxcm_rest.json 97 | .ipynb_checkpoints/ 98 | _pycache_/ 99 | rest_client.py 100 | fxcm_rest_gevent.py 101 | .gitignore 102 | python/fxcm_rest.json 103 | python/fxcm_rest.json 104 | -------------------------------------------------------------------------------- /RestApiNotebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "import requests" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import json\n", 21 | "from importlib import reload\n", 22 | "import fxcm_rest_api_token as fxcm_rest_api\n", 23 | "result = reload(fxcm_rest_api)" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": { 30 | "scrolled": true 31 | }, 32 | "outputs": [], 33 | "source": [ 34 | "trader = fxcm_rest_api.Trader('YOURKEYHERE', 'prod')\n", 35 | "trader.debug_level = \"INFO\" # verbose logging... don't set to receive errors only\n", 36 | "trader.login()" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "c =trader.candles(\"EUR/USD\", \"m15\", 45, dt_fmt=\"%Y/%m/%d %H:%M:%S\")['candles']\n", 46 | "print(len(c))\n", 47 | "for candle in c:\n", 48 | " print(candle)" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "help(trader.open_trade)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "res = trader.open_trade(trader.account_id, \"USD/JPY\", True, 1)\n", 67 | "orderId = res['data']['orderId']" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "tradeId = trader.get_tradeId(orderId)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": null, 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [ 85 | "print(tradeId)" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "trader.close_all_for_symbol(\"USD/JPY\")" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": { 101 | "collapsed": true 102 | }, 103 | "outputs": [], 104 | "source": [ 105 | "def print_candles(candle_data):\n", 106 | " candles = candle_data['candles']\n", 107 | " headers = candle_data['headers']\n", 108 | " print(\"{0[10]}, {0[1]}, {0[3]}, {0[4]}, {0[2]}\".format(headers))\n", 109 | " candles.reverse()\n", 110 | " for candle in candles:\n", 111 | " print(\"{0[10]}, {0[1]:0.6}, {0[3]:0.6}, {0[4]:0.6}, {0[2]:0.6}\".format(candle))" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "print_candles(cd)" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "print_candles(trader.get_candles(\"EUR/USD\", \"m1\", 5, dt_fmt=\"%Y/%m/%d %H:%M:%S\"))\n", 130 | "# time.sleep(60)\n", 131 | "# print(\"*\"*30)\n", 132 | "# print_candles(trader.get_candles(\"EUR/USD\", \"m1\", 5, dt_fmt=\"%Y/%m/%d %H:%M:%aS\"))" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": null, 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "trader.candles_as_dict(\"EUR/USD\", \"m1\", 1, dt_fmt=\"%Y/%m/%d %H:%M:%S\")\n", 142 | "\n" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [ 151 | "trader.close_all_for_symbol(trader.account_id, True, \"USD/JPY\", \"AtMarket\", \"GTC\")" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "trader.accounts" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": null, 166 | "metadata": {}, 167 | "outputs": [], 168 | "source": [ 169 | "trader.properties" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": null, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "print(trader.account_id)" 179 | ] 180 | }, 181 | { 182 | "cell_type": "markdown", 183 | "metadata": {}, 184 | "source": [ 185 | "#### Show how to supply a different handler for subscription items" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": null, 191 | "metadata": { 192 | "collapsed": true 193 | }, 194 | "outputs": [], 195 | "source": [ 196 | "\n", 197 | "def show(msg):\n", 198 | " '''\n", 199 | " Sample price handler. If on_price_update is registered for a symbol,\n", 200 | " it will update the symbol's values (stored in a symbol hash) with\n", 201 | " that price update.symbol hash.\n", 202 | "\n", 203 | " :return: none\n", 204 | " '''\n", 205 | " try:\n", 206 | " md = json.loads(msg)\n", 207 | " symbol = md[\"Symbol\"]\n", 208 | " t = trader\n", 209 | " si = trader.symbol_info.get(symbol, {})\n", 210 | " p_up = dict(symbol_info=t.symbol_info[symbol], parent=t)\n", 211 | " t.symbols[symbol] = t.symbols.get(symbol, fxcm_rest_api\n", 212 | " .PriceUpdate(p_up,\n", 213 | " symbol_info=si))\n", 214 | " trader.symbols[symbol].bid, trader.symbols[symbol].ask,\\\n", 215 | " trader.symbols[symbol].high,\\\n", 216 | " trader.symbols[symbol].low = md['Rates']\n", 217 | " trader.symbols[symbol].updated = md['Updated']\n", 218 | " print(t.symbols[symbol])\n", 219 | " except Exception as e:\n", 220 | " trader.logger.error(\"Can't handle price update: \" + str(e))\n", 221 | "\n", 222 | "# trader = fx" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": null, 228 | "metadata": { 229 | "scrolled": true 230 | }, 231 | "outputs": [], 232 | "source": [ 233 | "#trader.unsubscribe_symbol(\"EUR/USD\")\n", 234 | "trader.subscribe_symbol(\"EUR/USD\", handler=show)" 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": null, 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "trader.unsubscribe_symbol(\"EUR/USD\")" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "result = trader.subscribe_symbol(\"GBP/JPY\")\n", 253 | "print(result)\n", 254 | "print(trader.subscriptions)" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": null, 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "print(trader.symbols[\"GBP/JPY\"])\n", 264 | "print(trader.symbols[\"GBP/JPY\"].bid)\n", 265 | "print(trader.symbols[\"GBP/JPY\"].ask)" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": null, 271 | "metadata": {}, 272 | "outputs": [], 273 | "source": [ 274 | "print(trader.symbols['GBP/JPY'].parent)" 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": null, 280 | "metadata": {}, 281 | "outputs": [], 282 | "source": [ 283 | "trader.unsubscribe_symbol(\"GBP/JPY\")" 284 | ] 285 | }, 286 | { 287 | "cell_type": "code", 288 | "execution_count": null, 289 | "metadata": { 290 | "collapsed": true 291 | }, 292 | "outputs": [], 293 | "source": [ 294 | "trader.open_trade(trader.account_id, \"USD/JPY\", True, 1, at_market=1, time_in_force=\"FOK\")" 295 | ] 296 | }, 297 | { 298 | "cell_type": "code", 299 | "execution_count": null, 300 | "metadata": {}, 301 | "outputs": [], 302 | "source": [] 303 | } 304 | ], 305 | "metadata": { 306 | "kernelspec": { 307 | "display_name": "Python 2", 308 | "language": "python", 309 | "name": "python2" 310 | }, 311 | "language_info": { 312 | "codemirror_mode": { 313 | "name": "ipython", 314 | "version": 2 315 | }, 316 | "file_extension": ".py", 317 | "mimetype": "text/x-python", 318 | "name": "python", 319 | "nbconvert_exporter": "python", 320 | "pygments_lexer": "ipython2", 321 | "version": "2.7.12" 322 | } 323 | }, 324 | "nbformat": 4, 325 | "nbformat_minor": 2 326 | } 327 | -------------------------------------------------------------------------------- /fxcm_rest.json: -------------------------------------------------------------------------------- 1 | { 2 | "environments": { 3 | "demo": { 4 | "auth": "https://www-beta2.fxcorporate.com/oauth/token", 5 | "trading": "https://api-demo.fxcm.com", 6 | "port": 443 7 | } 8 | }, 9 | "logpath": "./logfile.txt", 10 | "_debugLevels": "Levels are (from most to least logging) DEBUG, INFO, WARNING, ERROR, CRITICAL", 11 | "debugLevel": "ERROR", 12 | "subscription_lists": "#Determines default subscription list of item updates to listen to", 13 | "subscription_list": ["Offer","Account","Order","OpenPosition","ClosedPosition", "LeverageProfile","Summary", 14 | "Properties"] 15 | } -------------------------------------------------------------------------------- /fxcm_rest_api.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import requests 3 | from socketIO_client import SocketIO 4 | import logging 5 | import json 6 | import uuid 7 | import threading 8 | from dateutil.parser import parse 9 | from datetime import datetime 10 | import time 11 | import types 12 | 13 | 14 | def isInt(v): 15 | v = str(v).strip() 16 | return v == '0' or (v if v.find('..') > -1 else v.lstrip('-+').rstrip('0') 17 | .rstrip('.')).isdigit() 18 | 19 | 20 | def timestamp_to_string(timestamp, datetime_fmt="%Y/%m/%d %H:%M:%S:%f"): 21 | return datetime.fromtimestamp(timestamp).strftime(datetime_fmt) 22 | 23 | 24 | class PriceUpdate(object): 25 | def __init__(self, bid=None, ask=None, high=None, low=None, updated=None, 26 | symbol_info=None, parent=None): 27 | self.bid = bid 28 | self.ask = ask 29 | self.high = high 30 | self.low = low 31 | self.updated = updated 32 | self.output_fmt = "%r" 33 | self.parent = parent 34 | if symbol_info is not None: 35 | self.symbol_info = symbol_info 36 | self.offer_id = symbol_info['offerId'] 37 | self.symbol = symbol_info['currency'] 38 | precision = symbol_info['ratePrecision'] / 10.0 39 | self.output_fmt = "%s%0.1ff" % ("%", precision) 40 | 41 | def __repr__(self): 42 | try: 43 | date = timestamp_to_string(self.updated) 44 | except Exception: 45 | date = None 46 | output = "PriceUpdate(bid={0}, ask={0}," \ 47 | "high={0}, low={0}, updated=%r)".format(self.output_fmt) 48 | return output % (self.bid, self.ask, self.high, 49 | self.low, date) 50 | 51 | def __print__(self): 52 | try: 53 | date = timestamp_to_string(self.updated) 54 | except Exception: 55 | date = None 56 | return "[%s => bid=%s, ask=%s, high=%s, low=%s]" % \ 57 | (self.bid, self.ask, self.high, self.low, date) 58 | 59 | def unsubscribe(self): 60 | return self.parent.unsubscribe_symbol(self.symbol) 61 | 62 | def resubscribe(self): 63 | return self.parent.subscribe_symbol(self.symbol) 64 | 65 | 66 | class Trader(object): 67 | '''FXCM REST API abstractor. 68 | Obtain a new instance of this class and use that 69 | to do all trade and account actions. 70 | ''' 71 | 72 | def __init__(self, access_token, environment, messageHandler=None, 73 | purpose='General', config_file="fxcm_rest.json"): 74 | self.config_file = config_file 75 | self.initialize() 76 | self.socketIO = None 77 | self.updates = {} 78 | self.symbols = {} 79 | self.symbol_info = {} 80 | self.symbol_id = {} 81 | self.account_id = None 82 | self.account_list = [] 83 | self.accounts = {} 84 | self.orders_list = {} 85 | self.trades = {} 86 | self.subscriptions = {} 87 | self.open_list = [] 88 | self.closed_list = [] 89 | self.currency_exposure = {} 90 | self.access_token = access_token 91 | self.env = environment 92 | self.purpose = purpose 93 | 94 | # for debugging - allows the suppression of specific messages 95 | # sent to self.Print.Helpful for when logging to console and 96 | # you want to keep log level, but remove some messages from the output 97 | self.ignore_output = [] 98 | ##### 99 | 100 | self.update_handlers = {"Offer": self.on_offer, 101 | "Account": self.on_account, 102 | "Order": self.on_order, 103 | "OpenPosition": self.on_openposition, 104 | "ClosedPosition": self.on_closedposition, 105 | "Summary": self.on_summary, 106 | "LeverageProfile": self.on_leverageprofile, 107 | "Properties": self.on_properties} 108 | 109 | if messageHandler is not None: 110 | self.message_handler = self.add_method(messageHandler) 111 | else: 112 | self.message_handler = self.on_message 113 | self.list = self.CONFIG.get('subscription_list', []) 114 | self.environment = self._get_config(environment) 115 | # self.login() 116 | 117 | def login(self): 118 | ''' 119 | Once you have an instance, run this method to log in to the service. 120 | Do this before any other calls 121 | :return: Dict 122 | ''' 123 | #self.socketIO = SocketIO(self.environment.get("trading"), 124 | # self.environment.get("port"), 125 | # params={'access_token': 126 | # self.access_token}) 127 | self._log_init() 128 | self.socketIO = SocketIO(self.environment.get("trading"), 129 | self.environment.get("port"), 130 | params={'access_token': 131 | self.access_token}) 132 | self.socketIO.on('connect', self.on_connect) 133 | self.socketIO.on('disconnect', self.on_disconnect) 134 | thread_name = self.access_token + self.env + self.purpose 135 | for thread in threading.enumerate(): 136 | if thread.name == thread_name: 137 | thread.keepGoing = False 138 | self._socketIO_thread = threading.Thread(target=self.socketIO.wait) 139 | self._socketIO_thread.setName(thread_name) 140 | self._socketIO_thread.keepGoing = True 141 | self._socketIO_thread.setDaemon(True) 142 | self._socketIO_thread.start() 143 | return self.__return(True, "Connecting") 144 | 145 | def bearerGen(self): 146 | return("Bearer " + self.socketIO._engineIO_session.id + self.access_token) 147 | 148 | def on_connect(self): 149 | ''' 150 | Actions to be preformed on login. By default will subscribe to updates 151 | for items defined in subscription_list. subscription_list is in the 152 | json self.CONFIG with options explained there. If messageHandler was 153 | passed on instantiation, that will be used to handle all messages. 154 | Alternatively, this method can be overridden before login is called to 155 | provide different on_connect functionality. 156 | 157 | :return: None 158 | ''' 159 | self.logger.info('Websocket connected: ' + 160 | self.socketIO._engineIO_session.id) 161 | self.bearer = self.bearerGen() 162 | self.HEADERS['Authorization'] = self.bearer 163 | accounts = self.get_model("Account").get('accounts', {}) 164 | self.account_list = [a['accountId'] for a in accounts] 165 | self.account_id = None 166 | for account in accounts: 167 | account_id = account['accountId'] 168 | self.accounts[account_id] = account 169 | if self.account_id is None and account_id != '': 170 | self.account_id = account_id 171 | 172 | self.get_offers() 173 | for item in self.list: 174 | handler = self.update_handlers.get(item, None) 175 | if handler is None: 176 | self.subscribe(item) 177 | else: 178 | self.subscribe(item, handler) 179 | 180 | def Print(self, message, message_type=None, level='INFO'): 181 | loggers = dict(INFO=self.logger.info, 182 | DEBUG=self.logger.debug, 183 | WARNING=self.logger.warning, 184 | ERROR=self.logger.error, 185 | CRITICAL=self.logger.critical) 186 | if message_type is None or message_type not in self.ignore_output: 187 | loggers[level](message) 188 | 189 | def add_method(self, method): 190 | ''' 191 | Returns a method suitable for addition to the instance. 192 | Can be used to override methods without subclassing. 193 | self.on_connect = self.add_method(MyConnectMethodHandler) 194 | :param method: 195 | :return: instance method 196 | ''' 197 | return types.MethodType(method, self) 198 | 199 | def logout(self): 200 | ''' 201 | Unsubscribes from all subscribed items and logs out. 202 | :return: 203 | ''' 204 | for item in self.subscriptions.keys(): 205 | self.subscriptions.pop(item) 206 | self.socketIO.off(item) 207 | self.send("/logout") 208 | 209 | def _loop(self): 210 | while self._socketIO_thread.keepGoing: 211 | self.socketIO.wait(1) 212 | 213 | def __exit__(self, *err): 214 | pass 215 | 216 | def __enter__(self): 217 | return self 218 | 219 | def __return(self, status, data): 220 | ret_value = {'status': status} 221 | if type(data) == dict: 222 | ret_value.update(data) 223 | else: 224 | ret_value.update({'data': data}) 225 | return ret_value 226 | 227 | def _send_request(self, method, command, params, additional_headers={}): 228 | self.HEADERS['Authorization'] = self.bearerGen() 229 | self.logger.info(self.environment.get( 230 | "trading") + command + str(params)) 231 | if method == 'get': 232 | rresp = requests.get(self.environment.get( 233 | "trading") + command, params=params, headers=self.HEADERS) 234 | else: 235 | # params = json.dumps(params) 236 | rresp = requests.post(self.environment.get( 237 | "trading") + command, headers=self.HEADERS, data=params) 238 | if rresp.status_code == 200: 239 | data = rresp.json() 240 | if data["response"]["executed"] is True: 241 | return self.__return(True, data) 242 | return self.__return(False, data["response"]["error"]) 243 | else: 244 | return self.__return(False, rresp.status_code) 245 | 246 | def send(self, location, params={}, method='post', additional_headers={}): 247 | ''' 248 | Method to send REST requests to the API 249 | 250 | :param location: eg. /subscribe 251 | :param params: eg. {"pairs": "USD/JPY"} 252 | :param method: GET, POST, DELETE, PATCH 253 | :return: response Dict 254 | ''' 255 | try: 256 | response = self._send_request( 257 | method, location, params, additional_headers) 258 | return response 259 | except Exception as e: 260 | self.logger.error("Failed to send request [%s]: %s" % (params, e)) 261 | status = False 262 | response = str(e) 263 | return self.__return(status, response) 264 | 265 | def _get_config(self, environment): 266 | ret = self.CONFIG.get("environments", {}).get(environment, {}) 267 | if ret == {}: 268 | self.logger.error( 269 | "No self.CONFIGuration found. Please call your trade object with\ 270 | 'get_self.CONFIG(environment)'.") 271 | self.logger.error("Environments are demo or real.") 272 | return ret 273 | 274 | def _log_init(self): 275 | self.logger = logging.getLogger( 276 | self.access_token + "_" + self.env + "_" + str(uuid.uuid4())[:8]) 277 | self.ch = logging.StreamHandler() 278 | self.set_log_level(self.debug_level) 279 | formatter = logging.Formatter( 280 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 281 | self.ch.setFormatter(formatter) 282 | self.logger.addHandler(self.ch) 283 | 284 | def _forget(self, subscribed_item): 285 | if subscribed_item in self.subscriptions: 286 | try: 287 | self.subscriptions.pop(subscribed_item) 288 | except Exception: 289 | pass 290 | 291 | def set_log_level(self, level): 292 | ''' 293 | set the logging level of the specific instance. 294 | Levels are DEBUG, INFO, WARNING, ERROR, CRITICAL 295 | :param level: Defaults to ERROR if log level is undefined 296 | 297 | :return: None 298 | ''' 299 | self.logger.setLevel(self.LOGLEVELS.get(level, "ERROR")) 300 | self.ch.setLevel(self.LOGLEVELS.get(level, "ERROR")) 301 | 302 | def _add_method(self): 303 | pass 304 | 305 | # Obtain and store the list of instruments in the symbol_info dict 306 | 307 | def get_offers(self): 308 | response = self.get_model("Offer") 309 | if response['status'] is True: 310 | for item in response['offers']: 311 | self.symbol_info[item['currency']] = item 312 | self.symbol_id[item['offerId']] = item['currency'] 313 | 314 | def on_disconnect(self): 315 | ''' 316 | Simply logs info of the socket being closed. 317 | Override to add functionality 318 | 319 | :return: None 320 | ''' 321 | self.logger.info("Websocket closed") 322 | 323 | def register_handler(self, message, handler): 324 | ''' 325 | Register a callback handler for a specified message type 326 | 327 | :param message: string 328 | :param handler: function 329 | :return: None 330 | ''' 331 | self.socketIO.on(message, handler) 332 | 333 | def on_price_update(self, msg): 334 | ''' 335 | Sample price handler. If on_price_update is registered for a symbol, 336 | it will update the symbol's values (stored in a symbol hash) with 337 | that price update.symbol hash. 338 | 339 | :return: none 340 | ''' 341 | try: 342 | md = json.loads(msg) 343 | symbol = md["Symbol"] 344 | symbol_info = self.symbol_info.get(symbol, {}) 345 | p_up = dict(symbol_info=self.symbol_info[symbol], parent=self) 346 | self.symbols[symbol] = self.symbols.get(symbol, PriceUpdate(p_up, 347 | symbol_info=symbol_info)) 348 | self.symbols[symbol].bid, self.symbols[symbol].ask,\ 349 | self.symbols[symbol].high,\ 350 | self.symbols[symbol].low = md['Rates'] 351 | self.symbols[symbol].updated = md['Updated'] 352 | except Exception as e: 353 | self.logger.error("Can't handle price update: " + str(e)) 354 | 355 | def on_offer(self, msg): 356 | message = json.loads(msg) 357 | self.Print("Offer Update:" + message, "Offer", "INFO") 358 | 359 | def on_account(self, msg): 360 | message = json.loads(msg) 361 | account_id = message['accountId'] 362 | self.accounts[account_id] = self.accounts.get(account_id, {}) 363 | self.accounts[account_id].update(message) 364 | # self.Print("Account Update:" + msg, "Account", "INFO") 365 | 366 | def on_order(self, msg): 367 | message = json.loads(msg) 368 | order_id = message.get('orderId', '') 369 | trade_id = message.get('tradeId', '') 370 | self.orders_list[order_id] = self.orders_list.get(order_id, 371 | {'actions': []}) 372 | if "action" in message: 373 | self.orders_list[order_id]['actions'].append(message) 374 | self.orders_list[order_id].update(message) 375 | self.Print("Order Update:" + msg, "Order", "INFO") 376 | 377 | def on_openposition(self, msg): 378 | message = json.loads(msg) 379 | self.Print("OpenPosition Update:" + msg, "OpenPosition", "INFO") 380 | 381 | def on_closedposition(self, msg): 382 | message = json.loads(msg) 383 | self.Print("ClosedPosition Update:" + msg, 384 | "ClosedPosition", "INFO") 385 | 386 | def on_summary(self, msg): 387 | message = json.loads(msg) 388 | self.Print("Summary Update:" + msg, "Summary", "INFO") 389 | 390 | def on_properties(self, msg): 391 | message = json.loads(msg) 392 | if "offerId" in message: 393 | message['symbol'] = self.symbol_id[message['offerId']] 394 | self.Print("Property Update:" + msg, "Property", "INFO") 395 | 396 | def on_leverageprofile(self, msg): 397 | message = json.loads(msg) 398 | self.Print("LeverageProfile Update:" + msg, 399 | "LeverageProfile", "INFO") 400 | 401 | def on_message(self, msg): 402 | ''' 403 | Sample generic message handling. 404 | Will update that specific message type with the latest message 405 | 406 | :return: 407 | ''' 408 | self.Print(msg, -1, "INFO") 409 | 410 | @property 411 | def summary(self): 412 | ''' 413 | Provides a summary snapshot ofthe account 414 | ''' 415 | return self.get_model("Summary").get('summary', []) 416 | 417 | @property 418 | def offers(self): 419 | return self.get_model("Offer").get('offers', []) 420 | 421 | @property 422 | def open_positions(self): 423 | return self.get_model('OpenPosition').get('open_positions', []) 424 | 425 | @property 426 | def closed_positions(self): 427 | return self.get_model('ClosedPosition').get('closed_positions', []) 428 | 429 | @property 430 | def orders(self): 431 | return self.get_model('Order').get('orders', []) 432 | 433 | @property 434 | def leverage_profile(self): 435 | return self.get_model("LeverageProfile").get('leverage_profile', []) 436 | 437 | @property 438 | def properties(self): 439 | return self.get_model("Properties").get('properties', []) 440 | 441 | def subscribe_symbol(self, instruments, handler=None): 442 | ''' 443 | Subscribe to given instrument 444 | 445 | :param instruments: 446 | :return: response Dict 447 | ''' 448 | handler = handler or self.on_price_update 449 | if type(instruments) is list: 450 | for instrument in instruments: 451 | self.subscriptions[instrument] = instrument 452 | self.socketIO.on(instrument, handler) 453 | else: 454 | self.subscriptions[instruments] = instruments 455 | self.socketIO.on(instruments, handler) 456 | return self.send("/subscribe", {"pairs": instruments}, 457 | additional_headers={'Transfer-Encoding': "chunked"}) 458 | 459 | def unsubscribe_symbol(self, instruments, 460 | headers={'Transfer-Encoding': "chunked"}): 461 | ''' 462 | Unsubscribe from instrument updates 463 | 464 | :param instruments: 465 | :return: response Dict 466 | ''' 467 | if type(instruments) is list: 468 | for instrument in instruments: 469 | self._forget(instrument) 470 | self.socketIO.off(instrument) 471 | else: 472 | self.socketIO.off(instruments) 473 | self._forget(instruments) 474 | return self.send("/unsubscribe", {"pairs": instruments}) 475 | 476 | def subscribe(self, items, handler=None): 477 | ''' 478 | Subscribes to the updates of the data models. 479 | Update will be pushed to client via socketIO 480 | Model choices: 'Offer', 'OpenPosition', 'ClosedPosition', 481 | 'Order', 'Account', 'Summary', 'LeverageProfile', 'Properties' 482 | 483 | :param item: 484 | :return: response Dict 485 | ''' 486 | handler = handler or self.on_message 487 | response = self.send("/trading/subscribe", {"models": items}) 488 | if response['status'] is True: 489 | if type(items) is list: 490 | for item in items: 491 | self.socketIO.on(item, handler) 492 | else: 493 | self.socketIO.on(items, handler) 494 | else: 495 | self.logger.error( 496 | "Error processing /trading/subscribe:" + str(response)) 497 | return response 498 | 499 | def unsubscribe(self, items): 500 | ''' 501 | Unsubscribe from model 502 | ["Offer","Account","Order","OpenPosition","Summary","Properties"] 503 | 504 | :param item: 505 | :return: response Dict 506 | ''' 507 | if type(items) is list: 508 | for item in items: 509 | self._forget(item) 510 | self.socketIO.off(item) 511 | else: 512 | self._forget(items) 513 | self.socketIO.off(items) 514 | return self.send("/trading/unsubscribe", {"models": items}) 515 | 516 | def get_tradeId(self, orderId): 517 | try: 518 | orderId = str(orderId) 519 | tradeId = None 520 | order = self.orders_list.get(orderId, {}) 521 | for action in order.get('actions', []): 522 | tradeId = action.get('tradeId', None) 523 | if tradeId is not None: 524 | break 525 | return tradeId 526 | except Exception as e: 527 | return {'Error': 'Error ' + str(e)} 528 | 529 | def get_model(self, item): 530 | ''' 531 | Gets current content snapshot of the specified data models. 532 | Model choices: 533 | 'Offer', 'OpenPosition', 'ClosedPosition', 'Order', 'Summary', 534 | 'LeverageProfile', 'Account', 'Properties' 535 | 536 | :param item: 537 | :return: response Dict 538 | ''' 539 | return self.send("/trading/get_model", {"models": item}, "get") 540 | 541 | def change_password(self, oldpwd, newpwd): 542 | ''' 543 | Change user password 544 | 545 | :param oldpwd: 546 | :param newpwd: 547 | :return: response Dict 548 | ''' 549 | return self.send("/trading/changePassword", {"oldPswd": oldpwd, 550 | "newPswd": newpwd, 551 | "confirmNewPswd": newpwd}) 552 | 553 | def permissions(self): 554 | ''' 555 | Gets the object which defines permissions for the specified account 556 | identifier and symbol. Each property of that object specifies the 557 | corresponding permission ("createEntry", "createMarket", 558 | "netStopLimit", "createOCO" and so on). 559 | The value of the property specifies the permission status 560 | ("disabled", "enabled" or "hidden") 561 | 562 | 563 | :param item: 564 | :return: response Dict 565 | ''' 566 | return self.send("/trading/permissions", {}, "get") 567 | 568 | def open_trade(self, account_id, symbol, is_buy, amount, rate=0, 569 | at_market=0, time_in_force="GTC", order_type="AtMarket", 570 | stop=None, trailing_step=None, limit=None, is_in_pips=None): 571 | ''' 572 | Create a Market Order with options for At Best or Market Range, 573 | and optional attached stops and limits. 574 | 575 | :param account_id: 576 | :param symbol: 577 | :param is_buy: 578 | :param amount: 579 | :param rate: 580 | :param at_market: 581 | :param time_in_force: 582 | :param order_type: 583 | :param stop: * Optional * 584 | :param trailing_step: * Optional * 585 | :param limit: * Optional * 586 | :param is_in_pips: * Optional * 587 | :return: response Dict 588 | ''' 589 | if None in [account_id, symbol, is_buy, amount]: 590 | ret = "Failed to provide mandatory parameters" 591 | return self.__return(False, ret) 592 | 593 | is_buy = 'true' if is_buy else 'false' 594 | is_in_pips = 'true' if is_in_pips else 'false' 595 | params = dict(account_id=account_id, symbol=symbol, 596 | is_buy=is_buy, amount=amount, rate=rate, 597 | at_market=at_market, time_in_force=time_in_force, 598 | order_type=order_type) 599 | if stop is not None: 600 | params['stop'] = stop 601 | 602 | if trailing_step is not None: 603 | params['trailing_step'] = trailing_step 604 | 605 | if limit is not None: 606 | params['limit'] = limit 607 | 608 | if is_in_pips is not None: 609 | params['is_in_pips'] = is_in_pips 610 | return self.send("/trading/open_trade", params) 611 | 612 | def close_trade(self, trade_id, amount, at_market=0, 613 | time_in_force="GTC", order_type="AtMarket", rate=None): 614 | ''' 615 | Close existing trade 616 | 617 | :param trade_id: 618 | :param amount: 619 | :param at_market: 620 | :param time_in_force: 621 | :param order_type: 622 | :param rate: * Optional * 623 | :return: response Dict 624 | ''' 625 | if None in [trade_id, amount, at_market, time_in_force, order_type]: 626 | ret = "Failed to provide mandatory parameters" 627 | return self.__return(False, ret) 628 | params = dict(trade_id=trade_id, amount=amount, at_market=at_market, 629 | time_in_force=time_in_force, order_type=order_type) 630 | if rate is not None: 631 | params['rate'] = rate 632 | return self.send("/trading/close_trade", params) 633 | 634 | def change_order(self, order_id, rate, rng, amount, trailing_step=None): 635 | ''' 636 | Change order rate/amount 637 | 638 | :param order_id: 639 | :param rate: 640 | :param range: 641 | :param amount: 642 | :param trailing_step: * Optional * 643 | :return: response Dict 644 | ''' 645 | if None in [order_id, amount, rate, rng]: 646 | ret = "Failed to provide mandatory parameters" 647 | return self.__return(False, ret) 648 | params = dict(order_id=order_id, rate=rate, range=rng, 649 | amount=amount) 650 | if trailing_step is not None: 651 | params['trailing_step'] = trailing_step 652 | return self.send("/trading/change_order", params) 653 | 654 | def delete_order(self, order_id): 655 | ''' 656 | Delete open order 657 | 658 | :param order_id: 659 | :return: response Dict 660 | ''' 661 | if None in [order_id]: 662 | ret = "Failed to provide mandatory parameters" 663 | return self.__return(False, ret) 664 | params = dict(order_id=order_id) 665 | return self.send("/trading/delete_order", params) 666 | 667 | def create_entry_order(self, account_id, symbol, is_buy, rate, amount, is_in_pips, 668 | order_type, time_in_force, limit=None, 669 | stop=None, trailing_step=None): 670 | """ 671 | Create a Limit Entry or a Stop Entry order. 672 | An order priced away from the market (not marketable) 673 | will be submitted as a Limit Entry order. An order priced through the 674 | market will be submitted as a Stop Entry order. 675 | 676 | If the market is at 1.1153 x 1.1159 677 | * Buy Entry order @ 1.1165 will be processed as a 678 | Buy Stop Entry order. 679 | * Buy Entry order @ 1.1154 will be processed as a 680 | Buy Limit Entry order 681 | 682 | :param account_id: 683 | :param symbol: 684 | :param is_buy: 685 | :param amount: 686 | :param limit: 687 | :param is_in_pips: 688 | :param order_type: 689 | :param time_in_force: 690 | :param rate: 691 | :param stop: * Optional * 692 | :param trailing_step: * Optional * 693 | :return: response Dict 694 | """ 695 | if None in [account_id, symbol, is_buy, amount, rate, 696 | is_in_pips, order_type, time_in_force]: 697 | ret = "Failed to provide mandatory parameters" 698 | return self.__return(False, ret) 699 | is_buy = 'true' if is_buy else 'false' 700 | is_in_pips = 'true' if is_in_pips else 'false' 701 | params = dict(account_id=account_id, symbol=symbol, is_buy=is_buy, 702 | amount=amount, rate=rate, is_in_pips=is_in_pips, 703 | time_in_force=time_in_force, order_type=order_type) 704 | if stop is not None: 705 | params['stop'] = stop 706 | if limit is not None: 707 | params['limit'] = limit 708 | if trailing_step is not None: 709 | params['trailing_step'] = trailing_step 710 | return self.send("/trading/create_entry_order", params) 711 | 712 | def simple_oco(self, account_id, symbol, amount, is_in_pips, time_in_force, 713 | expiration, is_buy, rate, stop, trailing_step, is_in_pips2, 714 | trailing_stop_step, limit, at_market, order_type, is_buy2, 715 | rate2, stop2, trailing_step2, trailing_stop_step2, limit2): 716 | ''' 717 | Create simple OCO 718 | 719 | :param account_id: 720 | :param symbol: 721 | :param amount: 722 | :param is_in_pips: 723 | :param time_in_force: 724 | :param expiration: 725 | :param is_buy: 726 | :param rate: 727 | :param stop: 728 | :param trailing_step: 729 | :param trailing_stop_step: 730 | :param limit: 731 | :param at_market: 732 | :param order_type: 733 | :param is_buy2: 734 | :param rate2: 735 | :param stop2: 736 | :param trailing_step2: 737 | :param trailing_stop_step2: 738 | :param limit2: 739 | :return: response Dict 740 | ''' 741 | items = locals().items() 742 | params = {} 743 | is_buy = 'true' if is_buy else 'false' 744 | is_in_pips = 'true' if is_in_pips else 'false' 745 | try: 746 | for k, v in items: 747 | if k != "self": 748 | params[k] = v 749 | return self.__return(self.send("/trading/simple_oco", params)) 750 | except Exception as e: 751 | return (False, str(e)) 752 | 753 | def add_to_oco(self, orderIds, ocoBulkId): 754 | ''' 755 | Add order(s) to an OCO 756 | 757 | :param orderIds: 758 | :param ocoBulkId: 759 | :return: response Dict 760 | ''' 761 | return self.send("/trading/add_to_oco", {"orderIds": orderIds, 762 | "ocoBulkId": ocoBulkId}) 763 | 764 | def remove_from_oco(self, orderIds): 765 | ''' 766 | Remove order(s) from OCO 767 | 768 | :param orderIds: 769 | :return: response Dict 770 | ''' 771 | return self.send("/trading/remove_from_oco", {"orderIds": orderIds}) 772 | 773 | def edit_oco(self, ocoBulkId, addOrderIds, removeIds): 774 | ''' 775 | Edit an OCO 776 | 777 | :param ocoBulkId: 778 | :param addOrderIds: 779 | :param removeOrderIds: 780 | :return: response Dict 781 | ''' 782 | return self.send("/trading/edit_oco", {"ocoBulkId": ocoBulkId, 783 | "addOrderIds": addOrderIds, 784 | "removeOrderIds": removeIds}) 785 | 786 | def change_trade_stop_limit(self, trade_id, is_stop, 787 | rate, is_in_pips=False, trailing_step=0): 788 | ''' 789 | Creates/removes/changes the stop/limit order for the specified trade. 790 | If the current stop/limit rate for the specified trade is not set 791 | (is zero) and the new rate is not zero, then creates a new order. 792 | If the current stop/limit rate for the specified trade is set 793 | (is not zero), changes order rate (if the new rate is not zero) or 794 | deletes order (if the new rate is zero). 795 | 796 | 797 | :param trade_id: 798 | :param is_stop: 799 | :param rate: 800 | :param is_in_pips: 801 | :param trailing_step: 802 | :return: response Dict 803 | ''' 804 | is_stop = 'true' if is_stop else 'false' 805 | is_in_pips = 'true' if is_in_pips else 'false' 806 | items = locals().items() 807 | params = {} 808 | try: 809 | for k, v in items: 810 | if k != "self": 811 | params[k] = v 812 | return self.send("/trading/change_trade_stop_limit", params) 813 | except Exception as e: 814 | return self.__return(False, str(e)) 815 | 816 | def change_order_stop_limit(self, order_id, limit, stop, 817 | is_limit_in_pips=False, is_stop_in_pips=False): 818 | ''' 819 | Creates/removes/changes the stop/limit order for the specified order. 820 | If the current stop/limit rate for the specified order is not set 821 | (is zero) and the new rate is not zero, 822 | then creates a new order. 823 | If the current stop/limit rate for the specified order is set 824 | (is not zero), changes order rate (if the new rate is not zero) 825 | or deletes order (if the new rate is zero). 826 | 827 | 828 | :param order_id: 829 | :param limit: 830 | :param stop: 831 | :param is_limit_in_pips: 832 | :param is_stop_in_pips: 833 | :return: response Dict 834 | ''' 835 | items = locals().items() 836 | params = {} 837 | is_stop_in_pips = 'true' if is_buy else 'false' 838 | is_limit_in_pips = 'true' if is_in_pips else 'false' 839 | try: 840 | for k, v in items: 841 | if k != "self": 842 | params[k] = v 843 | return self.send("/trading/change_order_stop_limit", params) 844 | except Exception as e: 845 | return self.__return(False, str(e)) 846 | 847 | def close_all_for_symbol(self, symbol, account_id=None, forSymbol=True, 848 | order_type="AtMarket", time_in_force="GTC"): 849 | ''' 850 | Closes all trades for the specified account and symbol by creating net 851 | quantity orders, if these orders are enabled, or by creating regular 852 | close orders otherwise. 853 | 854 | :param symbol: 855 | :param account_id: = None (default to self.account_id) 856 | :param forSymbol: True/False (Default True) 857 | :param order_type: AtMarket / MarketRange (default AtMarket) 858 | :param time_in_force: IOC GTC FOK DAY GTD (default GTC) 859 | :return: response Dict 860 | ''' 861 | items = locals().items() 862 | params = {} 863 | try: 864 | for k, v in items: 865 | if k != "self": 866 | if k == 'account_id' and v is None: 867 | v = self.account_id 868 | params[k] = v 869 | return self.send("/trading/close_all_for_symbol", params) 870 | except Exception as e: 871 | return self.__return(False, str(e)) 872 | 873 | def get_candles(self, instrument, period, num, 874 | From=None, To=None, dt_fmt=None): 875 | ''' 876 | Allow user to retrieve candle for a given instrument at a give time 877 | 878 | :param instrument: instrument_id or instrument. If instrument, will 879 | use mode information to convert to instrument_id 880 | :param period: m1, m5, m15, m30, H1, H2, H3, H4, H6, H8, D1, W1, M1 881 | :param num: candles, max = 10,000 882 | :param From: timestamp or date/time string. Will conver to timestamp 883 | :param To: timestamp or date/time string. Will conver to timestamp 884 | :param dt_fmt: Adding this optional parameter will add an additional 885 | field to the candle data with the timestamp converted 886 | to the datetime string provided. Example: 887 | .candles("USD/JPY", "m1", 3, datetime_fmt="%Y/%m/%d %H:%M:%S:%f") 888 | [1503694620, 109.321, 109.326, 109.326, 109.316, 109.359, 889 | 109.358, 109.362, 109.357, 28, '2017/08/26 05:57:00:000000'] 890 | :return: response Dict 891 | ''' 892 | try: 893 | initial_instrument = instrument 894 | if not isInt(instrument): 895 | instrument = self.symbol_info.get( 896 | instrument, {}).get('offerId', -1) 897 | if instrument < 0: 898 | raise ValueError("Instrument %s not found" % 899 | initial_instrument) 900 | if num > 10000: 901 | num = 10000 902 | params = dict(num=num) 903 | for k, v in {"From": From, "To": To}.items(): 904 | if v is not None: 905 | if not isInt(v): 906 | v = int(time.mktime(parse(v).timetuple())) 907 | params[k] = v 908 | candle_data = self.send("/candles/%s/%s" % 909 | (instrument, period), params, "get") 910 | headers = ['timestamp', 'bidopen', 'bidclose', 'bidhigh', 'bidlow', 911 | 'askopen', 'askclose', 'askhigh', 'asklow', 'tickqty'] 912 | if dt_fmt is not None: 913 | headers.append("datestring") 914 | for i, candle in enumerate(candle_data['candles']): 915 | candle_data['candles'][i].append( 916 | datetime.fromtimestamp(candle[0]).strftime(dt_fmt)) 917 | candle_data['headers'] = headers 918 | return self.__return(candle_data['status'], candle_data) 919 | except Exception as e: 920 | return (False, str(e)) 921 | 922 | candles = get_candles 923 | 924 | def candles_as_dict(self, instrument, period, num, 925 | From=None, To=None, dt_fmt=None): 926 | ''' 927 | Allow user to retrieve candle for a given instrument at a give time 928 | as a dictionary. 929 | 930 | :param instrument: instrument_id or instrument. If instrument, will 931 | use mode information to convert to instrument_id 932 | :param period: m1, m5, m15, m30, H1, H2, H3, H4, H6, H8, D1, W1, M1 933 | :param num: candles, max = 10,000 934 | :param From: timestamp or date/time string. Will conver to timestamp 935 | :param To: timestamp or date/time string. Will conver to timestamp 936 | :param dt_fmt: Adding this optional parameter will add an additional 937 | field to the candle data with the timestamp converted 938 | to the datetime string provided. Example: 939 | .candles("USD/JPY", "m1", 3, datetime_fmt="%Y/%m/%d %H:%M:%S:%f") 940 | [1503694620, 109.321, 109.326, 109.326, 109.316, 109.359, 941 | 109.358, 109.362, 109.357, 28, '2017/08/26 05:57:00:000000'] 942 | :return: response Dict 943 | ''' 944 | try: 945 | candle_data = self.get_candles( 946 | instrument, period, num, From, To, dt_fmt) 947 | status = candle_data['status'] 948 | if status is True: 949 | Headers = namedtuple('Headers', candle_data['headers']) 950 | candle_dict = map(Headers._make, candle_data['candles']) 951 | candle_data['candles'] = candle_dict 952 | return self.__return(status, candle_data) 953 | except Exception as e: 954 | return self.__return(False, str(e)) 955 | 956 | def initialize(self): 957 | self.HEADERS = { 958 | 'Accept': 'application/json', 959 | 'Content-Type': 'application/x-www-form-urlencoded', 960 | 'User-Agent': 'request' 961 | } 962 | self.CONFIG = {} 963 | try: 964 | with open(self.config_file, 'r') as f: 965 | self.CONFIG = json.load(f) 966 | except Exception as e: 967 | logging.error("Error loading self.CONFIG: " + str(e)) 968 | self.debug_level = self.CONFIG.get("DEBUGLEVEL", "ERROR") 969 | self.LOGLEVELS = {"ERROR": logging.ERROR, 970 | "DEBUG": logging.DEBUG, 971 | "INFO": logging.INFO, 972 | "WARNING": logging.WARNING, 973 | "CRITICAL": logging.CRITICAL} 974 | -------------------------------------------------------------------------------- /fxcm_rest_api_token.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import requests 3 | from socketIO_client import SocketIO 4 | import logging 5 | import json 6 | import uuid 7 | import threading 8 | from dateutil.parser import parse 9 | from datetime import datetime 10 | import time 11 | import types 12 | 13 | 14 | def isInt(v): 15 | v = str(v).strip() 16 | return v == '0' or (v if v.find('..') > -1 else v.lstrip('-+').rstrip('0') 17 | .rstrip('.')).isdigit() 18 | 19 | 20 | def timestamp_to_string(timestamp, datetime_fmt="%Y/%m/%d %H:%M:%S:%f"): 21 | return datetime.fromtimestamp(timestamp).strftime(datetime_fmt) 22 | 23 | 24 | class PriceUpdate(object): 25 | def __init__(self, bid=None, ask=None, high=None, low=None, updated=None, 26 | symbol_info=None, parent=None): 27 | self.bid = bid 28 | self.ask = ask 29 | self.high = high 30 | self.low = low 31 | self.updated = updated 32 | self.output_fmt = "%r" 33 | self.parent = parent 34 | if symbol_info is not None: 35 | self.symbol_info = symbol_info 36 | self.offer_id = symbol_info['offerId'] 37 | self.symbol = symbol_info['currency'] 38 | precision = symbol_info['ratePrecision'] / 10.0 39 | self.output_fmt = "%s%0.1ff" % ("%", precision) 40 | 41 | def __repr__(self): 42 | try: 43 | date = timestamp_to_string(self.updated) 44 | except Exception: 45 | date = None 46 | output = "PriceUpdate(bid={0}, ask={0}," \ 47 | "high={0}, low={0}, updated=%r)".format(self.output_fmt) 48 | return output % (self.bid, self.ask, self.high, 49 | self.low, date) 50 | 51 | def __print__(self): 52 | try: 53 | date = timestamp_to_string(self.updated) 54 | except Exception: 55 | date = None 56 | return "[%s => bid=%s, ask=%s, high=%s, low=%s]" % \ 57 | (self.bid, self.ask, self.high, self.low, date) 58 | 59 | def unsubscribe(self): 60 | return self.parent.unsubscribe_symbol(self.symbol) 61 | 62 | def resubscribe(self): 63 | return self.parent.subscribe_symbol(self.symbol) 64 | 65 | 66 | class Trader(object): 67 | '''FXCM REST API abstractor. 68 | Obtain a new instance of this class and use that 69 | to do all trade and account actions. 70 | ''' 71 | 72 | def __init__(self, access_token, environment, messageHandler=None, 73 | purpose='General', config_file="fxcm_rest.json"): 74 | self.config_file = config_file 75 | self.initialize() 76 | self.socketIO = None 77 | self.updates = {} 78 | self.symbols = {} 79 | self.symbol_info = {} 80 | self.symbol_id = {} 81 | self.account_id = None 82 | self.account_list = [] 83 | self.accounts = {} 84 | self.orders_list = {} 85 | self.trades = {} 86 | self.subscriptions = {} 87 | self.open_list = [] 88 | self.closed_list = [] 89 | self.currency_exposure = {} 90 | self.access_token = access_token 91 | self.env = environment 92 | self.purpose = purpose 93 | 94 | # for debugging - allows the suppression of specific messages 95 | # sent to self.Print.Helpful for when logging to console and 96 | # you want to keep log level, but remove some messages from the output 97 | self.ignore_output = [] 98 | ##### 99 | 100 | self.update_handlers = {"Offer": self.on_offer, 101 | "Account": self.on_account, 102 | "Order": self.on_order, 103 | "OpenPosition": self.on_openposition, 104 | "ClosedPosition": self.on_closedposition, 105 | "Summary": self.on_summary, 106 | "LeverageProfile": self.on_leverageprofile, 107 | "Properties": self.on_properties} 108 | 109 | if messageHandler is not None: 110 | self.message_handler = self.add_method(messageHandler) 111 | else: 112 | self.message_handler = self.on_message 113 | self.list = self.CONFIG.get('subscription_list', []) 114 | self.environment = self._get_config(environment) 115 | # self.login() 116 | 117 | def login(self): 118 | ''' 119 | Once you have an instance, run this method to log in to the service. 120 | Do this before any other calls 121 | :return: Dict 122 | ''' 123 | #self.socketIO = SocketIO(self.environment.get("trading"), 124 | # self.environment.get("port"), 125 | # params={'access_token': 126 | # self.access_token}) 127 | self._log_init() 128 | self.socketIO = SocketIO(self.environment.get("trading"), 129 | self.environment.get("port"), 130 | params={'access_token': 131 | self.access_token}) 132 | self.socketIO.on('connect', self.on_connect) 133 | self.socketIO.on('disconnect', self.on_disconnect) 134 | thread_name = self.access_token + self.env + self.purpose 135 | for thread in threading.enumerate(): 136 | if thread.name == thread_name: 137 | thread.keepGoing = False 138 | self._socketIO_thread = threading.Thread(target=self.socketIO.wait) 139 | self._socketIO_thread.setName(thread_name) 140 | self._socketIO_thread.keepGoing = True 141 | self._socketIO_thread.setDaemon(True) 142 | self._socketIO_thread.start() 143 | return self.__return(True, "Connecting") 144 | 145 | def bearerGen(self): 146 | return("Bearer " + self.socketIO._engineIO_session.id + self.access_token) 147 | 148 | def on_connect(self): 149 | ''' 150 | Actions to be preformed on login. By default will subscribe to updates 151 | for items defined in subscription_list. subscription_list is in the 152 | json self.CONFIG with options explained there. If messageHandler was 153 | passed on instantiation, that will be used to handle all messages. 154 | Alternatively, this method can be overridden before login is called to 155 | provide different on_connect functionality. 156 | 157 | :return: None 158 | ''' 159 | self.logger.info('Websocket connected: ' + 160 | self.socketIO._engineIO_session.id) 161 | self.bearer = self.bearerGen() 162 | self.HEADERS['Authorization'] = self.bearer 163 | accounts = self.get_model("Account").get('accounts', {}) 164 | self.account_list = [a['accountId'] for a in accounts] 165 | self.account_id = None 166 | for account in accounts: 167 | account_id = account['accountId'] 168 | self.accounts[account_id] = account 169 | if self.account_id is None and account_id != '': 170 | self.account_id = account_id 171 | 172 | self.get_offers() 173 | for item in self.list: 174 | handler = self.update_handlers.get(item, None) 175 | if handler is None: 176 | self.subscribe(item) 177 | else: 178 | self.subscribe(item, handler) 179 | 180 | def Print(self, message, message_type=None, level='INFO'): 181 | loggers = dict(INFO=self.logger.info, 182 | DEBUG=self.logger.debug, 183 | WARNING=self.logger.warning, 184 | ERROR=self.logger.error, 185 | CRITICAL=self.logger.critical) 186 | if message_type is None or message_type not in self.ignore_output: 187 | loggers[level](message) 188 | 189 | def add_method(self, method): 190 | ''' 191 | Returns a method suitable for addition to the instance. 192 | Can be used to override methods without subclassing. 193 | self.on_connect = self.add_method(MyConnectMethodHandler) 194 | :param method: 195 | :return: instance method 196 | ''' 197 | return types.MethodType(method, self) 198 | 199 | def logout(self): 200 | ''' 201 | Unsubscribes from all subscribed items and logs out. 202 | :return: 203 | ''' 204 | for item in self.subscriptions.keys(): 205 | self.subscriptions.pop(item) 206 | self.socketIO.off(item) 207 | self.send("/logout") 208 | 209 | def _loop(self): 210 | while self._socketIO_thread.keepGoing: 211 | self.socketIO.wait(1) 212 | 213 | def __exit__(self, *err): 214 | pass 215 | 216 | def __enter__(self): 217 | return self 218 | 219 | def __return(self, status, data): 220 | ret_value = {'status': status} 221 | if type(data) == dict: 222 | ret_value.update(data) 223 | else: 224 | ret_value.update({'data': data}) 225 | return ret_value 226 | 227 | def _send_request(self, method, command, params, additional_headers={}): 228 | self.HEADERS['Authorization'] = self.bearerGen() 229 | self.logger.info(self.environment.get( 230 | "trading") + command + str(params)) 231 | if method == 'get': 232 | rresp = requests.get(self.environment.get( 233 | "trading") + command, params=params, headers=self.HEADERS) 234 | else: 235 | # params = json.dumps(params) 236 | rresp = requests.post(self.environment.get( 237 | "trading") + command, headers=self.HEADERS, data=params) 238 | if rresp.status_code == 200: 239 | data = rresp.json() 240 | if data["response"]["executed"] is True: 241 | return self.__return(True, data) 242 | return self.__return(False, data["response"]["error"]) 243 | else: 244 | return self.__return(False, rresp.status_code) 245 | 246 | def send(self, location, params={}, method='post', additional_headers={}): 247 | ''' 248 | Method to send REST requests to the API 249 | 250 | :param location: eg. /subscribe 251 | :param params: eg. {"pairs": "USD/JPY"} 252 | :param method: GET, POST, DELETE, PATCH 253 | :return: response Dict 254 | ''' 255 | try: 256 | response = self._send_request( 257 | method, location, params, additional_headers) 258 | return response 259 | except Exception as e: 260 | self.logger.error("Failed to send request [%s]: %s" % (params, e)) 261 | status = False 262 | response = str(e) 263 | return self.__return(status, response) 264 | 265 | def _get_config(self, environment): 266 | ret = self.CONFIG.get("environments", {}).get(environment, {}) 267 | if ret == {}: 268 | self.logger.error( 269 | "No self.CONFIGuration found. Please call your trade object with\ 270 | 'get_self.CONFIG(environment)'.") 271 | self.logger.error("Environments are prod, dev or qa.") 272 | return ret 273 | 274 | def _log_init(self): 275 | self.logger = logging.getLogger( 276 | self.access_token + "_" + self.env + "_" + str(uuid.uuid4())[:8]) 277 | self.ch = logging.StreamHandler() 278 | self.set_log_level(self.debug_level) 279 | formatter = logging.Formatter( 280 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 281 | self.ch.setFormatter(formatter) 282 | self.logger.addHandler(self.ch) 283 | 284 | def _forget(self, subscribed_item): 285 | if subscribed_item in self.subscriptions: 286 | try: 287 | self.subscriptions.pop(subscribed_item) 288 | except Exception: 289 | pass 290 | 291 | def set_log_level(self, level): 292 | ''' 293 | set the logging level of the specific instance. 294 | Levels are DEBUG, INFO, WARNING, ERROR, CRITICAL 295 | :param level: Defaults to ERROR if log level is undefined 296 | 297 | :return: None 298 | ''' 299 | self.logger.setLevel(self.LOGLEVELS.get(level, "ERROR")) 300 | self.ch.setLevel(self.LOGLEVELS.get(level, "ERROR")) 301 | 302 | def _add_method(self): 303 | pass 304 | 305 | # Obtain and store the list of instruments in the symbol_info dict 306 | 307 | def get_offers(self): 308 | response = self.get_model("Offer") 309 | if response['status'] is True: 310 | for item in response['offers']: 311 | self.symbol_info[item['currency']] = item 312 | self.symbol_id[item['offerId']] = item['currency'] 313 | 314 | def on_disconnect(self): 315 | ''' 316 | Simply logs info of the socket being closed. 317 | Override to add functionality 318 | 319 | :return: None 320 | ''' 321 | self.logger.info("Websocket closed") 322 | 323 | def register_handler(self, message, handler): 324 | ''' 325 | Register a callback handler for a specified message type 326 | 327 | :param message: string 328 | :param handler: function 329 | :return: None 330 | ''' 331 | self.socketIO.on(message, handler) 332 | 333 | def on_price_update(self, msg): 334 | ''' 335 | Sample price handler. If on_price_update is registered for a symbol, 336 | it will update the symbol's values (stored in a symbol hash) with 337 | that price update.symbol hash. 338 | 339 | :return: none 340 | ''' 341 | try: 342 | md = json.loads(msg) 343 | symbol = md["Symbol"] 344 | symbol_info = self.symbol_info.get(symbol, {}) 345 | p_up = dict(symbol_info=self.symbol_info[symbol], parent=self) 346 | self.symbols[symbol] = self.symbols.get(symbol, PriceUpdate(p_up, 347 | symbol_info=symbol_info)) 348 | self.symbols[symbol].bid, self.symbols[symbol].ask,\ 349 | self.symbols[symbol].high,\ 350 | self.symbols[symbol].low = md['Rates'] 351 | self.symbols[symbol].updated = md['Updated'] 352 | except Exception as e: 353 | self.logger.error("Can't handle price update: " + str(e)) 354 | 355 | def on_offer(self, msg): 356 | message = json.loads(msg) 357 | self.Print("Offer Update:" + message, "Offer", "INFO") 358 | 359 | def on_account(self, msg): 360 | message = json.loads(msg) 361 | account_id = message['accountId'] 362 | self.accounts[account_id] = self.accounts.get(account_id, {}) 363 | self.accounts[account_id].update(message) 364 | # self.Print("Account Update:" + msg, "Account", "INFO") 365 | 366 | def on_order(self, msg): 367 | message = json.loads(msg) 368 | order_id = message.get('orderId', '') 369 | trade_id = message.get('tradeId', '') 370 | self.orders_list[order_id] = self.orders_list.get(order_id, 371 | {'actions': []}) 372 | if "action" in message: 373 | self.orders_list[order_id]['actions'].append(message) 374 | self.orders_list[order_id].update(message) 375 | self.Print("Order Update:" + msg, "Order", "INFO") 376 | 377 | def on_openposition(self, msg): 378 | message = json.loads(msg) 379 | self.Print("OpenPosition Update:" + msg, "OpenPosition", "INFO") 380 | 381 | def on_closedposition(self, msg): 382 | message = json.loads(msg) 383 | self.Print("ClosedPosition Update:" + msg, 384 | "ClosedPosition", "INFO") 385 | 386 | def on_summary(self, msg): 387 | message = json.loads(msg) 388 | self.Print("Summary Update:" + msg, "Summary", "INFO") 389 | 390 | def on_properties(self, msg): 391 | message = json.loads(msg) 392 | if "offerId" in message: 393 | message['symbol'] = self.symbol_id[message['offerId']] 394 | self.Print("Property Update:" + msg, "Property", "INFO") 395 | 396 | def on_leverageprofile(self, msg): 397 | message = json.loads(msg) 398 | self.Print("LeverageProfile Update:" + msg, 399 | "LeverageProfile", "INFO") 400 | 401 | def on_message(self, msg): 402 | ''' 403 | Sample generic message handling. 404 | Will update that specific message type with the latest message 405 | 406 | :return: 407 | ''' 408 | self.Print(msg, -1, "INFO") 409 | 410 | @property 411 | def summary(self): 412 | ''' 413 | Provides a summary snapshot ofthe account 414 | ''' 415 | return self.get_model("Summary").get('summary', []) 416 | 417 | @property 418 | def offers(self): 419 | return self.get_model("Offer").get('offers', []) 420 | 421 | @property 422 | def open_positions(self): 423 | return self.get_model('OpenPosition').get('open_positions', []) 424 | 425 | @property 426 | def closed_positions(self): 427 | return self.get_model('ClosedPosition').get('closed_positions', []) 428 | 429 | @property 430 | def orders(self): 431 | return self.get_model('Order').get('orders', []) 432 | 433 | @property 434 | def leverage_profile(self): 435 | return self.get_model("LeverageProfile").get('leverage_profile', []) 436 | 437 | @property 438 | def properties(self): 439 | return self.get_model("Properties").get('properties', []) 440 | 441 | def subscribe_symbol(self, instruments, handler=None): 442 | ''' 443 | Subscribe to given instrument 444 | 445 | :param instruments: 446 | :return: response Dict 447 | ''' 448 | handler = handler or self.on_price_update 449 | if type(instruments) is list: 450 | for instrument in instruments: 451 | self.subscriptions[instrument] = instrument 452 | self.socketIO.on(instrument, handler) 453 | else: 454 | self.subscriptions[instruments] = instruments 455 | self.socketIO.on(instruments, handler) 456 | return self.send("/subscribe", {"pairs": instruments}, 457 | additional_headers={'Transfer-Encoding': "chunked"}) 458 | 459 | def unsubscribe_symbol(self, instruments, 460 | headers={'Transfer-Encoding': "chunked"}): 461 | ''' 462 | Unsubscribe from instrument updates 463 | 464 | :param instruments: 465 | :return: response Dict 466 | ''' 467 | if type(instruments) is list: 468 | for instrument in instruments: 469 | self._forget(instrument) 470 | self.socketIO.off(instrument) 471 | else: 472 | self.socketIO.off(instruments) 473 | self._forget(instruments) 474 | return self.send("/unsubscribe", {"pairs": instruments}) 475 | 476 | def subscribe(self, items, handler=None): 477 | ''' 478 | Subscribes to the updates of the data models. 479 | Update will be pushed to client via socketIO 480 | Model choices: 'Offer', 'OpenPosition', 'ClosedPosition', 481 | 'Order', 'Account', 'Summary', 'LeverageProfile', 'Properties' 482 | 483 | :param item: 484 | :return: response Dict 485 | ''' 486 | handler = handler or self.on_message 487 | response = self.send("/trading/subscribe", {"models": items}) 488 | if response['status'] is True: 489 | if type(items) is list: 490 | for item in items: 491 | self.socketIO.on(item, handler) 492 | else: 493 | self.socketIO.on(items, handler) 494 | else: 495 | self.logger.error( 496 | "Error processing /trading/subscribe:" + str(response)) 497 | return response 498 | 499 | def unsubscribe(self, items): 500 | ''' 501 | Unsubscribe from model 502 | ["Offer","Account","Order","OpenPosition","Summary","Properties"] 503 | 504 | :param item: 505 | :return: response Dict 506 | ''' 507 | if type(items) is list: 508 | for item in items: 509 | self._forget(item) 510 | self.socketIO.off(item) 511 | else: 512 | self._forget(items) 513 | self.socketIO.off(items) 514 | return self.send("/trading/unsubscribe", {"models": items}) 515 | 516 | def get_tradeId(self, orderId): 517 | try: 518 | orderId = str(orderId) 519 | tradeId = None 520 | order = self.orders_list.get(orderId, {}) 521 | for action in order.get('actions', []): 522 | tradeId = action.get('tradeId', None) 523 | if tradeId is not None: 524 | break 525 | return tradeId 526 | except Exception as e: 527 | return {'Error': 'Error ' + str(e)} 528 | 529 | def get_model(self, item): 530 | ''' 531 | Gets current content snapshot of the specified data models. 532 | Model choices: 533 | 'Offer', 'OpenPosition', 'ClosedPosition', 'Order', 'Summary', 534 | 'LeverageProfile', 'Account', 'Properties' 535 | 536 | :param item: 537 | :return: response Dict 538 | ''' 539 | return self.send("/trading/get_model", {"models": item}, "get") 540 | 541 | def change_password(self, oldpwd, newpwd): 542 | ''' 543 | Change user password 544 | 545 | :param oldpwd: 546 | :param newpwd: 547 | :return: response Dict 548 | ''' 549 | return self.send("/trading/changePassword", {"oldPswd": oldpwd, 550 | "newPswd": newpwd, 551 | "confirmNewPswd": newpwd}) 552 | 553 | def permissions(self): 554 | ''' 555 | Gets the object which defines permissions for the specified account 556 | identifier and symbol. Each property of that object specifies the 557 | corresponding permission ("createEntry", "createMarket", 558 | "netStopLimit", "createOCO" and so on). 559 | The value of the property specifies the permission status 560 | ("disabled", "enabled" or "hidden") 561 | 562 | 563 | :param item: 564 | :return: response Dict 565 | ''' 566 | return self.send("/trading/permissions", {}, "get") 567 | 568 | def open_trade(self, account_id, symbol, is_buy, amount, rate=0, 569 | at_market=0, time_in_force="GTC", order_type="AtMarket", 570 | stop=None, trailing_step=None, limit=None, is_in_pips=None): 571 | ''' 572 | Create a Market Order with options for At Best or Market Range, 573 | and optional attached stops and limits. 574 | 575 | :param account_id: 576 | :param symbol: 577 | :param is_buy: 578 | :param amount: 579 | :param rate: 580 | :param at_market: 581 | :param time_in_force: 582 | :param order_type: 583 | :param stop: * Optional * 584 | :param trailing_step: * Optional * 585 | :param limit: * Optional * 586 | :param is_in_pips: * Optional * 587 | :return: response Dict 588 | ''' 589 | if None in [account_id, symbol, is_buy, amount]: 590 | ret = "Failed to provide mandatory parameters" 591 | return self.__return(False, ret) 592 | 593 | is_buy = 'true' if is_buy else 'false' 594 | is_in_pips = 'true' if is_in_pips else 'false' 595 | params = dict(account_id=account_id, symbol=symbol, 596 | is_buy=is_buy, amount=amount, rate=rate, 597 | at_market=at_market, time_in_force=time_in_force, 598 | order_type=order_type) 599 | if stop is not None: 600 | params['stop'] = stop 601 | 602 | if trailing_step is not None: 603 | params['trailing_step'] = trailing_step 604 | 605 | if limit is not None: 606 | params['limit'] = limit 607 | 608 | if is_in_pips is not None: 609 | params['is_in_pips'] = is_in_pips 610 | return self.send("/trading/open_trade", params) 611 | 612 | def close_trade(self, trade_id, amount, at_market=0, 613 | time_in_force="GTC", order_type="AtMarket", rate=None): 614 | ''' 615 | Close existing trade 616 | 617 | :param trade_id: 618 | :param amount: 619 | :param at_market: 620 | :param time_in_force: 621 | :param order_type: 622 | :param rate: * Optional * 623 | :return: response Dict 624 | ''' 625 | if None in [trade_id, amount, at_market, time_in_force, order_type]: 626 | ret = "Failed to provide mandatory parameters" 627 | return self.__return(False, ret) 628 | params = dict(trade_id=trade_id, amount=amount, at_market=at_market, 629 | time_in_force=time_in_force, order_type=order_type) 630 | if rate is not None: 631 | params['rate'] = rate 632 | return self.send("/trading/close_trade", params) 633 | 634 | def change_order(self, order_id, rate, rng, amount, trailing_step=None): 635 | ''' 636 | Change order rate/amount 637 | 638 | :param order_id: 639 | :param rate: 640 | :param range: 641 | :param amount: 642 | :param trailing_step: * Optional * 643 | :return: response Dict 644 | ''' 645 | if None in [order_id, amount, rate, rng]: 646 | ret = "Failed to provide mandatory parameters" 647 | return self.__return(False, ret) 648 | params = dict(order_id=order_id, rate=rate, range=rng, 649 | amount=amount) 650 | if trailing_step is not None: 651 | params['trailing_step'] = trailing_step 652 | return self.send("/trading/change_order", params) 653 | 654 | def delete_order(self, order_id): 655 | ''' 656 | Delete open order 657 | 658 | :param order_id: 659 | :return: response Dict 660 | ''' 661 | if None in [order_id]: 662 | ret = "Failed to provide mandatory parameters" 663 | return self.__return(False, ret) 664 | params = dict(order_id=order_id) 665 | return self.send("/trading/delete_order", params) 666 | 667 | def create_entry_order(self, account_id, symbol, is_buy, rate, amount, is_in_pips, 668 | order_type, time_in_force, limit=None, 669 | stop=None, trailing_step=None): 670 | """ 671 | Create a Limit Entry or a Stop Entry order. 672 | An order priced away from the market (not marketable) 673 | will be submitted as a Limit Entry order. An order priced through the 674 | market will be submitted as a Stop Entry order. 675 | 676 | If the market is at 1.1153 x 1.1159 677 | * Buy Entry order @ 1.1165 will be processed as a 678 | Buy Stop Entry order. 679 | * Buy Entry order @ 1.1154 will be processed as a 680 | Buy Limit Entry order 681 | 682 | :param account_id: 683 | :param symbol: 684 | :param is_buy: 685 | :param amount: 686 | :param limit: 687 | :param is_in_pips: 688 | :param order_type: 689 | :param time_in_force: 690 | :param rate: 691 | :param stop: * Optional * 692 | :param trailing_step: * Optional * 693 | :return: response Dict 694 | """ 695 | if None in [account_id, symbol, is_buy, amount, rate, 696 | is_in_pips, order_type, time_in_force]: 697 | ret = "Failed to provide mandatory parameters" 698 | return self.__return(False, ret) 699 | is_buy = 'true' if is_buy else 'false' 700 | is_in_pips = 'true' if is_in_pips else 'false' 701 | params = dict(account_id=account_id, symbol=symbol, is_buy=is_buy, 702 | amount=amount, rate=rate, is_in_pips=is_in_pips, 703 | time_in_force=time_in_force, order_type=order_type) 704 | if stop is not None: 705 | params['stop'] = stop 706 | if limit is not None: 707 | params['limit'] = limit 708 | if trailing_step is not None: 709 | params['trailing_step'] = trailing_step 710 | return self.send("/trading/create_entry_order", params) 711 | 712 | def simple_oco(self, account_id, symbol, amount, is_in_pips, time_in_force, 713 | expiration, is_buy, rate, stop, trailing_step, is_in_pips2, 714 | trailing_stop_step, limit, at_market, order_type, is_buy2, 715 | rate2, stop2, trailing_step2, trailing_stop_step2, limit2): 716 | ''' 717 | Create simple OCO 718 | 719 | :param account_id: 720 | :param symbol: 721 | :param amount: 722 | :param is_in_pips: 723 | :param time_in_force: 724 | :param expiration: 725 | :param is_buy: 726 | :param rate: 727 | :param stop: 728 | :param trailing_step: 729 | :param trailing_stop_step: 730 | :param limit: 731 | :param at_market: 732 | :param order_type: 733 | :param is_buy2: 734 | :param rate2: 735 | :param stop2: 736 | :param trailing_step2: 737 | :param trailing_stop_step2: 738 | :param limit2: 739 | :return: response Dict 740 | ''' 741 | items = locals().items() 742 | params = {} 743 | is_buy = 'true' if is_buy else 'false' 744 | is_in_pips = 'true' if is_in_pips else 'false' 745 | try: 746 | for k, v in items: 747 | if k != "self": 748 | params[k] = v 749 | return self.__return(self.send("/trading/simple_oco", params)) 750 | except Exception as e: 751 | return (False, str(e)) 752 | 753 | def add_to_oco(self, orderIds, ocoBulkId): 754 | ''' 755 | Add order(s) to an OCO 756 | 757 | :param orderIds: 758 | :param ocoBulkId: 759 | :return: response Dict 760 | ''' 761 | return self.send("/trading/add_to_oco", {"orderIds": orderIds, 762 | "ocoBulkId": ocoBulkId}) 763 | 764 | def remove_from_oco(self, orderIds): 765 | ''' 766 | Remove order(s) from OCO 767 | 768 | :param orderIds: 769 | :return: response Dict 770 | ''' 771 | return self.send("/trading/remove_from_oco", {"orderIds": orderIds}) 772 | 773 | def edit_oco(self, ocoBulkId, addOrderIds, removeIds): 774 | ''' 775 | Edit an OCO 776 | 777 | :param ocoBulkId: 778 | :param addOrderIds: 779 | :param removeOrderIds: 780 | :return: response Dict 781 | ''' 782 | return self.send("/trading/edit_oco", {"ocoBulkId": ocoBulkId, 783 | "addOrderIds": addOrderIds, 784 | "removeOrderIds": removeIds}) 785 | 786 | def change_trade_stop_limit(self, trade_id, is_stop, 787 | rate, is_in_pips=False, trailing_step=0): 788 | ''' 789 | Creates/removes/changes the stop/limit order for the specified trade. 790 | If the current stop/limit rate for the specified trade is not set 791 | (is zero) and the new rate is not zero, then creates a new order. 792 | If the current stop/limit rate for the specified trade is set 793 | (is not zero), changes order rate (if the new rate is not zero) or 794 | deletes order (if the new rate is zero). 795 | 796 | 797 | :param trade_id: 798 | :param is_stop: 799 | :param rate: 800 | :param is_in_pips: 801 | :param trailing_step: 802 | :return: response Dict 803 | ''' 804 | is_stop = 'true' if is_stop else 'false' 805 | is_in_pips = 'true' if is_in_pips else 'false' 806 | items = locals().items() 807 | params = {} 808 | try: 809 | for k, v in items: 810 | if k != "self": 811 | params[k] = v 812 | return self.send("/trading/change_trade_stop_limit", params) 813 | except Exception as e: 814 | return self.__return(False, str(e)) 815 | 816 | def change_order_stop_limit(self, order_id, limit, stop, 817 | is_limit_in_pips=False, is_stop_in_pips=False): 818 | ''' 819 | Creates/removes/changes the stop/limit order for the specified order. 820 | If the current stop/limit rate for the specified order is not set 821 | (is zero) and the new rate is not zero, 822 | then creates a new order. 823 | If the current stop/limit rate for the specified order is set 824 | (is not zero), changes order rate (if the new rate is not zero) 825 | or deletes order (if the new rate is zero). 826 | 827 | 828 | :param order_id: 829 | :param limit: 830 | :param stop: 831 | :param is_limit_in_pips: 832 | :param is_stop_in_pips: 833 | :return: response Dict 834 | ''' 835 | items = locals().items() 836 | params = {} 837 | is_stop_in_pips = 'true' if is_buy else 'false' 838 | is_limit_in_pips = 'true' if is_in_pips else 'false' 839 | try: 840 | for k, v in items: 841 | if k != "self": 842 | params[k] = v 843 | return self.send("/trading/change_order_stop_limit", params) 844 | except Exception as e: 845 | return self.__return(False, str(e)) 846 | 847 | def close_all_for_symbol(self, symbol, account_id=None, forSymbol=True, 848 | order_type="AtMarket", time_in_force="GTC"): 849 | ''' 850 | Closes all trades for the specified account and symbol by creating net 851 | quantity orders, if these orders are enabled, or by creating regular 852 | close orders otherwise. 853 | 854 | :param symbol: 855 | :param account_id: = None (default to self.account_id) 856 | :param forSymbol: True/False (Default True) 857 | :param order_type: AtMarket / MarketRange (default AtMarket) 858 | :param time_in_force: IOC GTC FOK DAY GTD (default GTC) 859 | :return: response Dict 860 | ''' 861 | items = locals().items() 862 | params = {} 863 | try: 864 | for k, v in items: 865 | if k != "self": 866 | if k == 'account_id' and v is None: 867 | v = self.account_id 868 | params[k] = v 869 | return self.send("/trading/close_all_for_symbol", params) 870 | except Exception as e: 871 | return self.__return(False, str(e)) 872 | 873 | def get_candles(self, instrument, period, num, 874 | From=None, To=None, dt_fmt=None): 875 | ''' 876 | Allow user to retrieve candle for a given instrument at a give time 877 | 878 | :param instrument: instrument_id or instrument. If instrument, will 879 | use mode information to convert to instrument_id 880 | :param period: m1, m5, m15, m30, H1, H2, H3, H4, H6, H8, D1, W1, M1 881 | :param num: candles, max = 10,000 882 | :param From: timestamp or date/time string. Will conver to timestamp 883 | :param To: timestamp or date/time string. Will conver to timestamp 884 | :param dt_fmt: Adding this optional parameter will add an additional 885 | field to the candle data with the timestamp converted 886 | to the datetime string provided. Example: 887 | .candles("USD/JPY", "m1", 3, datetime_fmt="%Y/%m/%d %H:%M:%S:%f") 888 | [1503694620, 109.321, 109.326, 109.326, 109.316, 109.359, 889 | 109.358, 109.362, 109.357, 28, '2017/08/26 05:57:00:000000'] 890 | :return: response Dict 891 | ''' 892 | try: 893 | initial_instrument = instrument 894 | if not isInt(instrument): 895 | instrument = self.symbol_info.get( 896 | instrument, {}).get('offerId', -1) 897 | if instrument < 0: 898 | raise ValueError("Instrument %s not found" % 899 | initial_instrument) 900 | if num > 10000: 901 | num = 10000 902 | params = dict(num=num) 903 | for k, v in {"From": From, "To": To}.items(): 904 | if v is not None: 905 | if not isInt(v): 906 | v = int(time.mktime(parse(v).timetuple())) 907 | params[k] = v 908 | candle_data = self.send("/candles/%s/%s" % 909 | (instrument, period), params, "get") 910 | headers = ['timestamp', 'bidopen', 'bidclose', 'bidhigh', 'bidlow', 911 | 'askopen', 'askclose', 'askhigh', 'asklow', 'tickqty'] 912 | if dt_fmt is not None: 913 | headers.append("datestring") 914 | for i, candle in enumerate(candle_data['candles']): 915 | candle_data['candles'][i].append( 916 | datetime.fromtimestamp(candle[0]).strftime(dt_fmt)) 917 | candle_data['headers'] = headers 918 | return self.__return(candle_data['status'], candle_data) 919 | except Exception as e: 920 | return (False, str(e)) 921 | 922 | candles = get_candles 923 | 924 | def candles_as_dict(self, instrument, period, num, 925 | From=None, To=None, dt_fmt=None): 926 | ''' 927 | Allow user to retrieve candle for a given instrument at a give time 928 | as a dictionary. 929 | 930 | :param instrument: instrument_id or instrument. If instrument, will 931 | use mode information to convert to instrument_id 932 | :param period: m1, m5, m15, m30, H1, H2, H3, H4, H6, H8, D1, W1, M1 933 | :param num: candles, max = 10,000 934 | :param From: timestamp or date/time string. Will conver to timestamp 935 | :param To: timestamp or date/time string. Will conver to timestamp 936 | :param dt_fmt: Adding this optional parameter will add an additional 937 | field to the candle data with the timestamp converted 938 | to the datetime string provided. Example: 939 | .candles("USD/JPY", "m1", 3, datetime_fmt="%Y/%m/%d %H:%M:%S:%f") 940 | [1503694620, 109.321, 109.326, 109.326, 109.316, 109.359, 941 | 109.358, 109.362, 109.357, 28, '2017/08/26 05:57:00:000000'] 942 | :return: response Dict 943 | ''' 944 | try: 945 | candle_data = self.get_candles( 946 | instrument, period, num, From, To, dt_fmt) 947 | status = candle_data['status'] 948 | if status is True: 949 | Headers = namedtuple('Headers', candle_data['headers']) 950 | candle_dict = map(Headers._make, candle_data['candles']) 951 | candle_data['candles'] = candle_dict 952 | return self.__return(status, candle_data) 953 | except Exception as e: 954 | return self.__return(False, str(e)) 955 | 956 | def initialize(self): 957 | self.HEADERS = { 958 | 'Accept': 'application/json', 959 | 'Content-Type': 'application/x-www-form-urlencoded', 960 | 'User-Agent': 'request' 961 | } 962 | self.CONFIG = {} 963 | try: 964 | with open(self.config_file, 'r') as f: 965 | self.CONFIG = json.load(f) 966 | except Exception as e: 967 | logging.error("Error loading self.CONFIG: " + str(e)) 968 | self.debug_level = self.CONFIG.get("DEBUGLEVEL", "ERROR") 969 | self.LOGLEVELS = {"ERROR": logging.ERROR, 970 | "DEBUG": logging.DEBUG, 971 | "INFO": logging.INFO, 972 | "WARNING": logging.WARNING, 973 | "CRITICAL": logging.CRITICAL} 974 | -------------------------------------------------------------------------------- /fxcm_rest_client_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import fxcm_rest_api_token as fxcm_rest_api 3 | import time 4 | 5 | trader = fxcm_rest_api.Trader('YOURTOKEN', 'demo') # demo for demo 6 | trader.login() 7 | try: 8 | print("Logged in, now getting Account details") 9 | while len(trader.account_list) < 1: 10 | time.sleep(0.1) 11 | account_id = trader.account_list[0] 12 | print(trader.account_id == account_id) 13 | print("Opening a trade now -USD/JPY 10 lots on %s" % account_id) 14 | response = trader.open_trade(account_id, "USD/JPY", True, 10) 15 | print(response) 16 | if response['status'] is True: 17 | orderId = response['data']['orderId'] 18 | tradeId = trader.get_tradeId(orderId) 19 | print("TradeID: ", tradeId) 20 | print("Open trade response: ", response) 21 | positions = trader.get_model("OpenPosition") 22 | print("Positions: ", positions) 23 | response = trader.close_all_for_symbol("USD/JPY") 24 | print("Close All result:\n\n", response['status'], response, "\n\n") 25 | positions = trader.get_model("OpenPosition") 26 | print("Positions: ", positions) 27 | 28 | c = trader.candles("EUR/USD", "m15", 15, dt_fmt="%Y/%m/%d %H:%M:%S")['candles'] 29 | print(len(c)) 30 | for candle in c: 31 | print(candle) 32 | 33 | 34 | c = trader.get_candles("USD/JPY", "M1", 10) 35 | for candle in c['candles']: 36 | print(candle) 37 | except Exception as e: 38 | print(str(e)) 39 | 40 | 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Python REST API 2 | 3 | After cloning this repository: 4 | 5 | For a quick demo: 6 | ----------------- 7 | 1. Install python 8 | 2. Run: pip install -r requirements.txt 9 | 3. Change token in rest_client.py file 10 | 4. Within the fxcm_rest.json file: 11 | * Set log path via the logpath field 12 | * Set the authentication client_id and client_secret details. 13 | * Set debugLevel if desired 14 | * Set subscription lists if desired 15 | 5. In the fxcm_rest_client_sample.py file: 16 | * Set your token and environment (demo/real) 17 | 18 | For a Notebook demo: 19 | -------------------- 20 | 1. Install Python 21 | 2. Run: pip install jupyter < if you don't have jupyter installed already> 22 | 3. Run: pip install -r requirements.txt 23 | 4. In this directory run: jupyter notebook 24 | 5. Start the RestApiNotebook.ipynb. 25 | 26 | ## Details 27 | 28 | This API exposes the methods of the REST API as a class, dealing with all of the common tasks 29 | involved with setting up connections and wiring callback listeners for you. In addition to that 30 | there are a few convenience methods. 31 | A quick example is as follows; 32 | 33 | import fxcm_rest_api_token 34 | import time 35 | trader = fxcm_rest_api_token.Trader('YOURTOKEN', 'prod') 36 | trader.login() 37 | 38 | #### Open Market Order 39 | # query account details and use the first account found 40 | accounts = trader.get_model("Account") 41 | account_id = accounts['accounts'][0]['accountId'] 42 | # Open 10 lots on USD/JPY for the first account_id found. 43 | response = trader.open_trade(account_id, "USD/JPY", True, 10) 44 | if response['status']: 45 | # close all USD/JPY trades. 46 | response = trader.close_all_for_symbol("USD/JPY") 47 | 48 | #### Historical Data request 49 | basic = trader.candles("USD/JPY", "m1", 5) 50 | print(basic) 51 | date_fmt = trader.candles("USD/JPY", "m1", 5, dt_fmt="%Y/%m/%d %H:%M:%S") 52 | print(date_fmt) 53 | date_fmt_headers = trader.candles_as_dict("USD/JPY", "m1", 3, dt_fmt="%Y/%m/%d %H:%M:%S") 54 | print(date_fmt_headers) 55 | ##### Price subscriptions 56 | subscription_result = trader.subscribe_symbol("USD/JPY") 57 | 58 | # Define alternative price update handler and supply that. 59 | def pupdate(msg): 60 | print("Price update: ", msg) 61 | subscription_result = trader.subscribe_symbol("USD/JPY", pupdate) 62 | counter = 1 63 | while counter < 60: 64 | time.sleep(1) 65 | counter += 1 66 | 67 | (All calls to candles allow either instrument name, or offerId. They also allow the From and To to be specified 68 | as timestamp or a date/time format that will be interpreted ("2017/08/01 10:00", "Aug 1, 2017 10:00", etc.). 69 | In addition to instrument_id, response, period_id and candles, a 'headers' field (not documented in the API notes) 70 | is returned, representing the candle fields.) 71 | 72 | basic 73 | 74 | for item in basic['candles']: 75 | print item 76 | 77 | [1503694500, 109.317, 109.336, 109.336, 109.314, 109.346, 109.366, 109.373, 109.344, 72] 78 | [1503694560, 109.336, 109.321, 109.337, 109.317, 109.366, 109.359, 109.366, 109.354, 83] 79 | [1503694620, 109.321, 109.326, 109.326, 109.316, 109.359, 109.358, 109.362, 109.357, 28] 80 | date_fmt 81 | 82 | for item in date_fmt['candles']: 83 | print item 84 | 85 | [1503694500, 109.317, 109.336, 109.336, 109.314, 109.346, 109.366, 109.373, 109.344, 72, '2017/08/26 05:55:00'] 86 | [1503694560, 109.336, 109.321, 109.337, 109.317, 109.366, 109.359, 109.366, 109.354, 83, '2017/08/26 05:56:00'] 87 | [1503694620, 109.321, 109.326, 109.326, 109.316, 109.359, 109.358, 109.362, 109.357, 28, '2017/08/26 05:57:00'] 88 | date_fmt_headers 89 | 90 | for item in date_fmt_headers['candles']: 91 | print item 92 | 93 | Headers(timestamp=1503694620, bidopen=109.321, bidclose=109.326, bidhigh=109.326, bidlow=109.316, askopen=109.359, askclose=109.358, askhigh=109.362, asklow=109.357, tickqty=28, datestring='2017/08/26 05:57:00') 94 | Headers(timestamp=1503694680, bidopen=109.326, bidclose=109.312, bidhigh=109.326, bidlow=109.31, askopen=109.358, askclose=109.374, askhigh=109.376, asklow=109.358, tickqty=42, datestring='2017/08/26 05:58:00') 95 | Headers(timestamp=1503694740, bidopen=109.312, bidclose=109.312, bidhigh=109.312, bidlow=109.31, askopen=109.374, askclose=109.374, askhigh=109.374, asklow=109.372, tickqty=4, datestring='2017/08/26 05:59:00') 96 | 97 | for item in date_fmt_headers['candles']: 98 | print "%s: Ask Close [%s], High Bid [%s] " % (item.datestring, item.askclose, item.bidhigh) 99 | 100 | 2017/08/26 05:57:00: Ask Close [109.358], High Bid [109.326] 101 | 2017/08/26 05:58:00: Ask Close [109.374], High Bid [109.326] 102 | 2017/08/26 05:59:00: Ask Close [109.374], High Bid [109.312] 103 | subscribe_symbol - default 104 | 105 | {u'Updated': 1504167080, u'Rates': [110.467, 110.488, 110.629, 110.156], u'Symbol': u'USD/JPY'} 106 | {u'Updated': 1504167081, u'Rates': [110.469, 110.49, 110.629, 110.156], u'Symbol': u'USD/JPY'} 107 | subscribe_symbol - overridden 108 | 109 | Price update: {"Updated":1504167248,"Rates":[110.446,110.468,110.629,110.156],"Symbol":"USD/JPY"} 110 | Price update: {"Updated":1504167250,"Rates":[110.446,110.468,110.629,110.156],"Symbol":"USD/JPY"} 111 | 112 | 113 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | socketIO-client --------------------------------------------------------------------------------