├── util ├── __init__.py └── cfLogging.py ├── requirements.txt ├── .gitignore ├── ReadMe.md ├── cfWebSocketApiV1Examples.py └── cfWebSocketApiV1.py /util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six==1.12.0 2 | websocket-client==0.54.0 -------------------------------------------------------------------------------- /util/cfLogging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | LOGGING_LEVEL = "DEBUG" 5 | 6 | LOG_IN_STDOUT = True 7 | LOG_IN_FILE = False 8 | LOG_PATH = "..." 9 | LOG_FILENAME = "..." 10 | 11 | class CfLogger(object): 12 | 13 | @staticmethod 14 | def get_logger(name): 15 | logger = logging.getLogger(name) 16 | logger.setLevel(LOGGING_LEVEL) 17 | formatter = logging.Formatter('[%(asctime)s] [%(levelname)5s] [%(threadName)10s] [%(name)10s] %(message)s') 18 | 19 | if LOG_IN_FILE and LOG_PATH and LOG_FILENAME: 20 | file_handler = logging.FileHandler("{0}/{1}.log".format(LOG_PATH, LOG_FILENAME), mode="a") 21 | file_handler.setFormatter(formatter) 22 | logger.addHandler(file_handler) 23 | 24 | if LOG_IN_STDOUT: 25 | ch = logging.StreamHandler() 26 | ch.setFormatter(formatter) 27 | logger.addHandler(ch) 28 | 29 | return logger 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff: 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/dictionaries 8 | 9 | # Sensitive or high-churn files: 10 | .idea/**/dataSources/ 11 | .idea/**/dataSources.ids 12 | .idea/**/dataSources.xml 13 | .idea/**/dataSources.local.xml 14 | .idea/**/sqlDataSources.xml 15 | .idea/**/dynamic.xml 16 | .idea/**/uiDesigner.xml 17 | 18 | # Gradle: 19 | .idea/**/gradle.xml 20 | .idea/**/libraries 21 | 22 | # CMake 23 | cmake-build-debug/ 24 | cmake-build-release/ 25 | 26 | # Mongo Explorer plugin: 27 | .idea/**/mongoSettings.xml 28 | 29 | ## File-based project format: 30 | *.iws 31 | 32 | ## Plugin-specific files: 33 | 34 | # IntelliJ 35 | out/ 36 | 37 | # mpeltonen/sbt-idea plugin 38 | .idea_modules/ 39 | 40 | # JIRA plugin 41 | atlassian-ide-plugin.xml 42 | 43 | # Cursive Clojure plugin 44 | .idea/replstate.xml 45 | 46 | # Crashlytics plugin (for Android Studio and IntelliJ) 47 | com_crashlytics_export_strings.xml 48 | crashlytics.properties 49 | crashlytics-build.properties 50 | fabric.properties 51 | 52 | 53 | # Byte-compiled / optimized / DLL files 54 | __pycache__/ 55 | *.py[cod] 56 | 57 | # C extensions 58 | *.so 59 | 60 | # Distribution / packaging 61 | bin/ 62 | build/ 63 | develop-eggs/ 64 | dist/ 65 | eggs/ 66 | lib/ 67 | lib64/ 68 | parts/ 69 | sdist/ 70 | var/ 71 | *.egg-info/ 72 | .installed.cfg 73 | *.egg 74 | 75 | # Installer logs 76 | pip-log.txt 77 | pip-delete-this-directory.txt 78 | 79 | # Unit test / coverage reports 80 | .tox/ 81 | .coverage 82 | .cache 83 | nosetests.xml 84 | coverage.xml 85 | 86 | # Translations 87 | *.mo 88 | 89 | # Mr Developer 90 | .mr.developer.cfg 91 | .project 92 | .pydevproject 93 | 94 | # Rope 95 | .ropeproject 96 | 97 | # Django stuff: 98 | *.log 99 | *.pot 100 | 101 | # Sphinx documentation 102 | docs/_build/ 103 | 104 | # CryptoF 105 | log/ 106 | bin 107 | develop-eggs 108 | dist 109 | downloads 110 | eggs 111 | parts 112 | src/*.egg-info 113 | lib 114 | lib64 115 | cfApiProperties.py 116 | *.iml 117 | settings.py -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | Crypto Facilities Websocket API v1 2 | ================================== 3 | 4 | This is a sample web socket application for [Crypto Facilities Ltd](https://www.cryptofacilities.com/), to demonstrate 5 | the new WS API. 6 | 7 | 8 | Getting Started 9 | --------------- 10 | 11 | 1. Amend the `cfWebSocketApiV1Examples.py` file to enter your api keys 12 | 1. Install the required libraries with ```$ pip install -r requirements.txt``` 13 | 1. Run the example application with ```$ python cfWebSocketApiV1Examples.py``` 14 | 15 | Functionality Overview 16 | ---------------------- 17 | 18 | * This application subscribes to all available feeds 19 | 20 | 21 | Application Sample Output 22 | ------------------------- 23 | 24 | The following is some of what you can expect when running this application: 25 | 26 | ``` 27 | [2018-02-01 20:29:41,968] [ INFO] [ Thread-1] [ cf-ws-api] Connected to ws://localhost:8080/ws/v1 28 | [2018-02-01 20:29:41,970] [ INFO] [ Thread-1] [ cf-ws-api] {'event': 'info', 'version': 1} 29 | [2018-02-01 20:29:42,950] [ INFO] [MainThread] [ cf-ws-api] public subscribe to trade 30 | [2018-02-01 20:29:42,956] [ INFO] [ Thread-1] [ cf-ws-api] {'event': 'subscribed', 'feed': 'trade', 'product_ids': ['FV_XRPXBT_180615']} 31 | [2018-02-01 20:29:42,974] [ INFO] [MainThread] [ cf-ws-api] public subscribe to book 32 | [2018-02-01 20:29:42,977] [ INFO] [ Thread-1] [ cf-ws-api] {'event': 'subscribed', 'feed': 'book', 'product_ids': ['FV_XRPXBT_180615']} 33 | [2018-02-01 20:29:42,978] [ INFO] [MainThread] [ cf-ws-api] public subscribe to ticker 34 | [2018-02-01 20:29:42,982] [ INFO] [ Thread-1] [ cf-ws-api] {'feed': 'ticker_snapshot', 'product_id': 'FV_XRPXBT_180615', 'bid': 2.562e-05, 'ask': 0.0, 'bid_size': 5900.0, 'ask_size': 0.0, 'volume': 0.0, 'dtm': 133, 'leverage': '6x', 'index': 0.00010452, 'premium': 0.0, 'last': 2.673e-05, 'time': 1517509363842, 'change': 0.0} 35 | [2018-02-01 20:29:42,982] [ INFO] [MainThread] [ cf-ws-api] public subscribe to ticker_lite 36 | [2018-02-01 20:29:42,983] [ INFO] [ Thread-1] [ cf-ws-api] {'event': 'subscribed', 'feed': 'ticker', 'product_ids': ['FV_XRPXBT_180615']} 37 | [2018-02-01 20:29:42,985] [ INFO] [ Thread-1] [ cf-ws-api] {'event': 'subscribed', 'feed': 'ticker_lite', 'product_ids': ['FV_XRPXBT_180615']} 38 | [2018-02-01 20:29:42,986] [ INFO] [ Thread-1] [ cf-ws-api] {'feed': 'ticker_lite_snapshot', 'product_id': 'FV_XRPXBT_180615', 'bid': 2.562e-05, 'ask': 0.0, 'change': 0.0, 'premium': 0.0, 'volume': 0.0, 'index': 0.0} 39 | [2018-02-01 20:29:42,990] [ INFO] [MainThread] [ cf-ws-api] waiting for challenge... 40 | [2018-02-01 20:29:42,997] [ INFO] [ Thread-1] [ cf-ws-api] {'event': 'challenge', 'message': '5a4ab830-e67f-4c0c-8565-e922f07122fb'} 41 | ``` -------------------------------------------------------------------------------- /cfWebSocketApiV1Examples.py: -------------------------------------------------------------------------------- 1 | # Crypto Facilities Ltd Web Socket API V1 2 | 3 | # Copyright (c) 2018 Crypto Facilities 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the "Software"), 7 | # to deal in the Software without restriction, including without limitation 8 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | # and/or sell copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included 13 | # in all 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 LIABILITY, 19 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 20 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | import cfWebSocketApiV1 as cfWsApi 23 | import util.cfLogging as cfLog 24 | logger = cfLog.CfLogger.get_logger(" Example ") 25 | 26 | 27 | ######################################################################################################################## 28 | # Please insert you API key and secret 29 | ######################################################################################################################## 30 | 31 | api_path = "wss://www.cryptofacilities.com/ws/v1" 32 | api_key = "..." # accessible on your Account page under Settings -> API Keys 33 | api_secret = "..." # accessible on your Account page under Settings -> API Keys 34 | timeout = 10 35 | trace = False # set to True for connection verbose logging 36 | 37 | cfWs = cfWsApi.CfWebSocketMethods(base_url=api_path, api_key=api_key, api_secret=api_secret, timeout=10, trace=trace) 38 | 39 | 40 | def subscribe_api_tester(): 41 | """Test the subscribe methods""" 42 | 43 | ##### public feeds ##### 44 | 45 | product_ids = ["PI_XBTUSD"] 46 | 47 | # subscribe to trade 48 | feed = "trade" 49 | cfWs.subscribe_public(feed, product_ids) 50 | 51 | # subscribe to book 52 | feed = "book" 53 | cfWs.subscribe_public(feed, product_ids) 54 | 55 | # subscribe to ticker 56 | feed = "ticker" 57 | cfWs.subscribe_public(feed, product_ids) 58 | 59 | # subscribe to ticker lite 60 | feed = "ticker_lite" 61 | cfWs.subscribe_public(feed, product_ids) 62 | 63 | # subscribe to heartbeat 64 | feed = "heartbeat" 65 | cfWs.subscribe_public(feed) 66 | 67 | 68 | ##### private feeds ##### 69 | 70 | # subscribe to account balances and margis 71 | feed = "account_balances_and_margins" 72 | cfWs.subscribe_private(feed) 73 | 74 | # subscribe to account log 75 | feed = "account_log" 76 | cfWs.subscribe_private(feed) 77 | 78 | # subscribe to deposits withdrawals 79 | feed = "deposits_withdrawals" 80 | cfWs.subscribe_private(feed) 81 | 82 | # subscribe to fills 83 | feed = "fills" 84 | cfWs.subscribe_private(feed) 85 | 86 | # subscribe to open positions 87 | feed = "open_positions" 88 | cfWs.subscribe_private(feed) 89 | 90 | # subscribe to open orders 91 | feed = "open_orders" 92 | cfWs.subscribe_private(feed) 93 | 94 | # subscribe to notifications 95 | feed = "notifications_auth" 96 | cfWs.subscribe_private(feed) 97 | 98 | 99 | def unsubscribe_api_tester(): 100 | """Test the unsubscribe methods""" 101 | 102 | ##### public feeds ##### 103 | 104 | product_ids = ["FV_XRPXBT_180615"] 105 | 106 | # unsubscribe to trade 107 | feed = "trade" 108 | cfWs.unsubscribe_public(feed, product_ids) 109 | 110 | # unsubscribe to book 111 | feed = "book" 112 | cfWs.unsubscribe_public(feed, product_ids) 113 | 114 | # unsubscribe to ticker 115 | feed = "ticker" 116 | cfWs.unsubscribe_public(feed, product_ids) 117 | 118 | # unsubscribe to ticker lite 119 | feed = "ticker_lite" 120 | cfWs.unsubscribe_public(feed, product_ids) 121 | 122 | # unsubscribe to heartbeat 123 | feed = "heartbeat" 124 | cfWs.unsubscribe_public(feed) 125 | 126 | ##### private feeds ##### 127 | 128 | # unsubscribe to account balances and margins 129 | feed = "account_balances_and_margins" 130 | cfWs.unsubscribe_private(feed) 131 | 132 | # unsubscribe to account log 133 | feed = "account_log" 134 | cfWs.unsubscribe_private(feed) 135 | 136 | # unsubscribe to deposits withdrawals 137 | feed = "deposits_withdrawals" 138 | cfWs.unsubscribe_private(feed) 139 | 140 | # unsubscribe to fills 141 | feed = "fills" 142 | cfWs.unsubscribe_private(feed) 143 | 144 | # unsubscribe to open positions 145 | feed = "open_positions" 146 | cfWs.unsubscribe_private(feed) 147 | 148 | # unsubscribe to open orders 149 | feed = "open_orders" 150 | cfWs.unsubscribe_private(feed) 151 | 152 | # unsubscribe to notifications 153 | feed = "notifications_auth" 154 | cfWs.unsubscribe_private(feed) 155 | 156 | 157 | logger.info("-----------------------------------------------------------") 158 | logger.info("****PRESS ANY KEY TO SUBSCRIBE AND START RECEIVING INFO****") 159 | logger.info("-----------------------------------------------------------") 160 | input() 161 | 162 | # Subscribe 163 | subscribe_api_tester() 164 | logger.info("-----------------------------------------------------------") 165 | logger.info("****PRESS ANY KEY TO UNSUBSCRIBE AND EXIT APPLICATION****") 166 | logger.info("-----------------------------------------------------------") 167 | input() 168 | 169 | # Unsubscribe 170 | unsubscribe_api_tester() 171 | 172 | # Exit 173 | exit() 174 | 175 | -------------------------------------------------------------------------------- /cfWebSocketApiV1.py: -------------------------------------------------------------------------------- 1 | # Crypto Facilities Ltd Web Socket API V1 2 | 3 | # Copyright (c) 2018 Crypto Facilities 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the "Software"), 7 | # to deal in the Software without restriction, including without limitation 8 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | # and/or sell copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included 13 | # in all 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 LIABILITY, 19 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 20 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | 23 | import json 24 | import hashlib 25 | import base64 26 | import hmac 27 | import sys 28 | import websocket 29 | 30 | from time import sleep 31 | from threading import Thread 32 | from util.cfLogging import CfLogger 33 | 34 | class CfWebSocketMethods(object): 35 | """Crypto Facilities Ltd Web Socket API Connector""" 36 | 37 | # Special Methods 38 | 39 | def __init__(self, base_url, api_key="", api_secret="", timeout=5, trace=False): 40 | websocket.enableTrace(trace) 41 | self.logger = CfLogger.get_logger("cf-ws-api") 42 | self.base_url = base_url 43 | self.api_key = api_key 44 | self.api_secret = api_secret 45 | self.timeout = timeout 46 | 47 | self.ws = None 48 | self.original_challenge = None 49 | self.signed_challenge = None 50 | self.challenge_ready = False 51 | 52 | self.__connect() 53 | 54 | # Public feeds 55 | def subscribe_public(self, feed, product_ids=None): 56 | """Subscribe to given feed and product ids""" 57 | 58 | if product_ids is None: 59 | request_message = { 60 | "event": "subscribe", 61 | "feed": feed 62 | } 63 | else: 64 | request_message = { 65 | "event": "subscribe", 66 | "feed": feed, 67 | "product_ids": product_ids 68 | } 69 | 70 | self.logger.info("public subscribe to %s", feed) 71 | 72 | request_json = json.dumps(request_message) 73 | self.ws.send(request_json) 74 | 75 | def unsubscribe_public(self, feed, product_ids=None): 76 | """UnSubscribe to given feed and product ids""" 77 | 78 | 79 | if product_ids is None: 80 | request_message = { 81 | "event": "unsubscribe", 82 | "feed": feed 83 | } 84 | else: 85 | request_message = { 86 | "event": "unsubscribe", 87 | "feed": feed, 88 | "product_ids": product_ids 89 | } 90 | 91 | self.logger.info("public unsubscribe to %s", feed) 92 | request_json = json.dumps(request_message) 93 | self.ws.send(request_json) 94 | 95 | # Private feeds 96 | def subscribe_private(self, feed): 97 | """Unsubscribe to feed""" 98 | 99 | if not self.challenge_ready: 100 | self.__wait_for_challenge_auth() 101 | 102 | request_message = {"event": "subscribe", 103 | "feed": feed, 104 | "api_key": self.api_key, 105 | "original_challenge": self.original_challenge, 106 | "signed_challenge": self.signed_challenge} 107 | 108 | self.logger.info("private subscribe to %s", feed) 109 | 110 | request_json = json.dumps(request_message) 111 | self.ws.send(request_json) 112 | 113 | def unsubscribe_private(self, feed): 114 | """Unsubscribe to feed""" 115 | 116 | if not self.challenge_ready: 117 | self.__wait_for_challenge_auth() 118 | 119 | request_message = {"event": "unsubscribe", 120 | "feed": feed, 121 | "api_key": self.api_key, 122 | "original_challenge": self.original_challenge, 123 | "signed_challenge": self.signed_challenge} 124 | 125 | self.logger.info("private unsubscribe to %s", feed) 126 | 127 | request_json = json.dumps(request_message) 128 | self.ws.send(request_json) 129 | 130 | def __connect(self): 131 | """Establish a web socket connection""" 132 | self.ws = websocket.WebSocketApp(self.base_url, 133 | on_message=self.__on_message, 134 | on_close=self.__on_close, 135 | on_open=self.__on_open, 136 | on_error=self.__on_error, 137 | ) 138 | 139 | self.wst = Thread(target=lambda: self.ws.run_forever(ping_interval=30)) 140 | self.wst.daemon = True 141 | self.wst.start() 142 | 143 | # Wait for connect before continuing 144 | conn_timeout = self.timeout 145 | while (not self.ws.sock or not self.ws.sock.connected) and conn_timeout: 146 | sleep(1) 147 | conn_timeout -= 1 148 | 149 | if not conn_timeout: 150 | self.logger.info("Couldn't connect to", self.base_url, "! Exiting.") 151 | sys.exit(1) 152 | 153 | def __on_open(self): 154 | self.logger.info("Connected to %s", self.base_url) 155 | 156 | def __on_message(self, message): 157 | """Listen the web socket connection. Block until a message 158 | arrives. """ 159 | 160 | message_json = json.loads(message) 161 | self.logger.info(message_json) 162 | 163 | if message_json.get("event", "") == "challenge": 164 | self.original_challenge = message_json["message"] 165 | self.signed_challenge = self.__sign_challenge(self.original_challenge) 166 | self.challenge_ready = True 167 | 168 | def __on_close(self): 169 | self.logger.info('Connection closed') 170 | 171 | def __on_error(self, error): 172 | self.logger.info(error) 173 | 174 | def __wait_for_challenge_auth(self): 175 | self.__request_challenge() 176 | 177 | self.logger.info("waiting for challenge...") 178 | while not self.challenge_ready: 179 | sleep(1) 180 | 181 | def __request_challenge(self): 182 | """Request a challenge from Crypto Facilities Ltd""" 183 | 184 | request_message = { 185 | "event": "challenge", 186 | "api_key": self.api_key 187 | } 188 | 189 | request_json = json.dumps(request_message) 190 | self.ws.send(request_json) 191 | 192 | def __sign_challenge(self, challenge): 193 | """Signed a challenge received from Crypto Facilities Ltd""" 194 | # step 1: hash the message with SHA256 195 | sha256_hash = hashlib.sha256() 196 | sha256_hash.update(challenge.encode("utf8")) 197 | hash_digest = sha256_hash.digest() 198 | 199 | # step 3: base64 decode apiPrivateKey 200 | secret_decoded = base64.b64decode(self.api_secret) 201 | 202 | # step 4: use result of step 3 to has the result of step 2 with HMAC-SHA512 203 | hmac_digest = hmac.new(secret_decoded, hash_digest, hashlib.sha512).digest() 204 | 205 | # step 5: base64 encode the result of step 4 and return 206 | sch = base64.b64encode(hmac_digest).decode("utf-8") 207 | return sch 208 | 209 | --------------------------------------------------------------------------------