diff --git a/README.rst b/README.rst index 355d65a3..fa7954d3 100644 --- a/README.rst +++ b/README.rst @@ -135,6 +135,41 @@ To receive results in ``pandas`` format, use the ``get_dataframe()`` method: startDate='2017-01-01', endDate='2018-05-31') +Websocket support:: + +.. code-block:: python + from tiingo import TiingoWebsocketClient + + def cb_fn(msg): + + # Example response + # msg = { + # "service":"iex" # An identifier telling you this is IEX data. + # The value returned by this will correspond to the endpoint argument. + # + # # Will always return "A" meaning new price quotes. There are also H type Heartbeat msgs used to keep the connection alive + # "messageType":"A" # A value telling you what kind of data packet this is from our IEX feed. + # + # # see https://api.tiingo.com/documentation/websockets/iex > Response for more info + # "data":[] # an array containing trade information and a timestamp + # + # } + + print(msg) + + subscribe = { + 'eventName':'subscribe', + 'authorization':'API_KEY_GOES_HERE', + #see https://api.tiingo.com/documentation/websockets/iex > Request for more info + 'eventData': { + 'thresholdLevel':5 + } + } + # notice how the object isn't needed after using it + # any logic should be implemented in the callback function + TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) + while True:pass + You can specify any of the end of day frequencies (daily, weekly, monthly, and annually) or any intraday frequency for both the ``get_ticker_price`` and ``get_dataframe`` methods. Weekly frequencies resample to the end of day on Friday, monthly frequencies resample to the last day of the month, and annually frequencies resample to the end of day on 12-31 of each year. The intraday frequencies are specified using an integer followed by "Min" or "Hour", for example "30Min" or "1Hour". diff --git a/docs/usage.rst b/docs/usage.rst index 60ef7e2b..408f572c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -61,6 +61,41 @@ Now you can use ``TiingoClient`` to make your API calls. (Other parameters are a startDate='2017-01-01', endDate='2017-08-31') +Websocket support:: + +.. code-block:: python + from tiingo import TiingoWebsocketClient + + def cb_fn(msg): + + # Example response + # msg = { + # "service":"iex" # An identifier telling you this is IEX data. + # The value returned by this will correspond to the endpoint argument. + # + # # Will always return "A" meaning new price quotes. There are also H type Heartbeat msgs used to keep the connection alive + # "messageType":"A" # A value telling you what kind of data packet this is from our IEX feed. + # + # # see https://api.tiingo.com/documentation/websockets/iex > Response for more info + # "data":[] # an array containing trade information and a timestamp + # + # } + + print(msg) + + subscribe = { + 'eventName':'subscribe', + 'authorization':'API_KEY_GOES_HERE', + #see https://api.tiingo.com/documentation/websockets/iex > Request for more info + 'eventData': { + 'thresholdLevel':5 + } + } + # notice how the object isn't needed after using it + # any logic should be implemented in the callback function + TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) + while True:pass + Further Docs -------- diff --git a/setup.py b/setup.py index 5ac90986..a08e6316 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ def read(*filenames, **kwargs): requirements = [ 'requests', + 'websocket-client' ] setup_requirements = [ diff --git a/tests/test_wsclient.py b/tests/test_wsclient.py new file mode 100644 index 00000000..af3b7b8b --- /dev/null +++ b/tests/test_wsclient.py @@ -0,0 +1,44 @@ +import os +from unittest import TestCase,mock +from tiingo.wsclient import TiingoWebsocketClient +from tiingo.exceptions import MissingRequiredArgumentError + +class TestRestClientWithSession(TestCase): + def setUp(self): + + def msg_cb(msg): + print(msg) + + self.cb=msg_cb + + self.config = { + 'eventName':'subscribe', + 'authorization':os.getenv("TIINGO_API_KEY"), + #see https://api.tiingo.com/documentation/websockets/iex > Request for more info + 'eventData': { + 'thresholdLevel':5 + } + } + + # test for missing or incorrectly supplied endpoints + def test_missing_or_wrong_endpoint(self): + with self.assertRaises(AttributeError) as ex: + TiingoWebsocketClient(config=self.config,on_msg_cb=self.cb) + self.assertTrue(type(ex.exception)==AttributeError) + + with self.assertRaises(AttributeError) as ex: + TiingoWebsocketClient(config=self.config,endpoint='wq',on_msg_cb=self.cb) + self.assertTrue(type(ex.exception)==AttributeError) + + # test for missing callback argument + def test_missing_msg_cb(self): + with self.assertRaises(MissingRequiredArgumentError) as ex: + TiingoWebsocketClient(config=self.config,endpoint='iex') + self.assertTrue(type(ex.exception)==MissingRequiredArgumentError) + + # test for missing API keys in config dict and in os env + def test_missing_api_key(self): + with mock.patch.dict(os.environ, {}, clear=True): #clear env vars including the TIINGO_API_KEY + with self.assertRaises(RuntimeError) as ex: + TiingoWebsocketClient(config={},endpoint='iex',on_msg_cb=self.cb) + self.assertTrue(type(ex.exception)==RuntimeError) diff --git a/tiingo/__init__.py b/tiingo/__init__.py index be6190be..21594440 100644 --- a/tiingo/__init__.py +++ b/tiingo/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from tiingo.api import TiingoClient +from tiingo.wsclient import TiingoWebsocketClient __author__ = """Cameron Yick""" __email__ = 'cameron.yick@enigma.com' diff --git a/tiingo/wsclient.py b/tiingo/wsclient.py new file mode 100644 index 00000000..ffeefe30 --- /dev/null +++ b/tiingo/wsclient.py @@ -0,0 +1,96 @@ +import os +import websocket +import json +from tiingo.exceptions import MissingRequiredArgumentError + +class TiingoWebsocketClient: + ''' + from tiingo import TiingoWebsocketClient + + def cb_fn(msg): + + # Example response + # msg = { + # "service":"iex" # An identifier telling you this is IEX data. + # The value returned by this will correspond to the endpoint argument. + # + # # Will always return "A" meaning new price quotes. There are also H type Heartbeat msgs used to keep the connection alive + # "messageType":"A" # A value telling you what kind of data packet this is from our IEX feed. + # + # # see https://api.tiingo.com/documentation/websockets/iex > Response for more info + # "data":[] # an array containing trade information and a timestamp + # + # } + + print(msg) + + subscribe = { + 'eventName':'subscribe', + 'authorization':'API_KEY_GOES_HERE', + #see https://api.tiingo.com/documentation/websockets/iex > Request for more info + 'eventData': { + 'thresholdLevel':5 + } + } + # notice how the object isn't needed after using it + # any logic should be implemented in the callback function + TiingoWebsocketClient(subscribe,endpoint="iex",on_msg_cb=cb_fn) + while True:pass + ''' + + def __init__(self,config=None,endpoint=None,on_msg_cb=None): + + self._base_url = "wss://api.tiingo.com" + self.config = {} if config is None else config + + try: + api_key = self.config['authorization'] + except KeyError: + api_key = os.environ.get('TIINGO_API_KEY') + self.config.update({"authorization":api_key}) + + self._api_key = api_key + if not(api_key): + raise RuntimeError("Tiingo API Key not provided. Please provide" + " via environment variable or config argument." + "Notice that this config dict takes the API Key as authorization ") + + self.endpoint = endpoint + if not (self.endpoint=="iex" or self.endpoint=="fx" or self.endpoint=="crypto"): + raise AttributeError("Endpoint must be defined as either (iex,fx,crypto) ") + + self.on_msg_cb = on_msg_cb + if not self.on_msg_cb: + raise MissingRequiredArgumentError("please define on_msg_cb It's a callback that gets called when new messages arrive " + "Example:" + "def cb_fn(msg):" + " print(msg)") + + websocket.enableTrace(False) + + ws = websocket.WebSocketApp("{0}/{1}".format(self._base_url,self.endpoint), + on_message = self.get_on_msg_cb(), + on_error = self.on_error, + on_close = self.on_close, + on_open = self.get_on_open(self.config)) + ws.run_forever() + + def get_on_open(self,config): + # the methods passed to websocketClient have to be unbounded if we want WebSocketApp to pass everything correctly + # see websocket-client/#471 + def on_open(ws): + ws.send(json.dumps(config)) + return on_open + + def get_on_msg_cb(self): + def on_msg_cb_local(ws,msg): + self.on_msg_cb(msg) + return + return on_msg_cb_local + + # since methods need to be unbound in order for websocketClient these methods don't have a self as their first parameter + def on_error(ws, error): # lgtm[py/not-named-self] + print(error) + + def on_close(ws): # lgtm[py/not-named-self] + pass