From ddc830bf41fa2dba6f5f8fcc2c8cc4670dfa97c3 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 01:44:38 -0700 Subject: [PATCH 01/65] create a model for an option instrument --- pyrh/models/option.py | 80 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 pyrh/models/option.py diff --git a/pyrh/models/option.py b/pyrh/models/option.py new file mode 100644 index 00000000..b1d4d454 --- /dev/null +++ b/pyrh/models/option.py @@ -0,0 +1,80 @@ +"""Current portfolio.""" + +from typing import Any + +from marshmallow import fields, post_load + +from pyrh.common import JSON + +from .base import BaseModel, BaseSchema + + +class Option(BaseModel): + """Robinhood Option data class. Represents an options instrument""" + + pass + + +class OptionSchema(BaseSchema): + """Robinhood Option schema data loader. + Sample result payload from + { + "chain_id": "cee01a93-626e-4ee6-9b04-60e2fd1392d1", + "chain_symbol": "AAPL", + "created_at": "2020-03-31T01:27:43.249339Z", + "expiration_date": "2020-04-17", + "id": "f098f169-74f9-4b91-b955-6834e1b67a12", + "issue_date": "2004-11-29", + "min_ticks": { + "above_tick": "0.05", + "below_tick": "0.01", + "cutoff_price": "3.00" + }, + "rhs_tradability": "untradable", + "state": "active", + "strike_price": "232.5000", + "tradability": "tradable", + "type": "put", + "updated_at": "2020-03-31T01:27:43.249354Z", + "url": "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" + } + """ + + __model__ = Options + + chain_id = fields.String() + chain_symbol = fields.String() + created_at = fields.NaiveDateTime() + expiration_date = fields.Date() + options_id = fields.String() + issue_date = fields.Date() + min_ticks = fields.Dict() + rhs_tradability = fields.String() + state = fields.String() + strike_price = fields.Float() + tradability = fields.String() + options_type = fields.String() + updated_at = fields.NaiveDateTime() + url = fields.URL() + + @post_load + def make_object(self, data: JSON, **kwargs: Any) -> Option: + """Build model for the Option class. + + Args: + data: The JSON diction to use to build the Option. + **kwargs: Unused but required to match signature of `Schema.make_object` + + Returns: + An instance of the Option class. + + """ + # Can potentially move this preprocessing part to a helper file + prefix = self.__class__.__name__.lower() + reserved_word_overwrite = ["type", "id"] + for attr in reserved_word_overwrite: + k, v = f"{prefix}_{attr}", data.pop("attr") + data[k] = v + + data = data.get("results", [{}])[0] + return self.__model__(**data) From 6bb7c2c2a8741145524657fcc98b22b20a9311ca Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 01:48:19 -0700 Subject: [PATCH 02/65] add options arg for endpoints, refactor get_instruments - todo: add **kwargs for more criteria on matching --- pyrh/endpoints.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/pyrh/endpoints.py b/pyrh/endpoints.py index a81386e2..05969767 100755 --- a/pyrh/endpoints.py +++ b/pyrh/endpoints.py @@ -37,18 +37,13 @@ def edocuments(): return API_BASE.with_path("/documents/") -def instruments(instrument_id=None, option=None): +def instruments(options=False): """ Return information about a specific instrument by providing its instrument id. Add extra options for additional information such as "popularity" """ - url = API_BASE.with_path(f"/instruments/") - if instrument_id is not None: - url += f"{instrument_id}" - if option is not None: - url += f"{option}" - - return url + opt = "/options" if options else "" + return API_BASE.with_path(f"{opt}/instruments/") def margin_upgrades(): @@ -63,9 +58,9 @@ def notifications(): return API_BASE.with_path("/notifications/") -def orders(order_id=""): - return API_BASE.with_path(f"/orders/{order_id}/") - +def orders(order_id="", options=False): + opt = "/options" if options else "" + return base.with_path(f"{opt}/orders/{order_id}/") def password_reset(): return API_BASE.with_path("/password_reset/request/") @@ -75,16 +70,19 @@ def portfolios(): return API_BASE.with_path("/portfolios/") -def positions(): - return API_BASE.with_path("/positions/") +def positions(options=False): + opt = "/options" if options else "" + return API_BASE.with_path("{opt}/positions/") def quotes(): return API_BASE.with_path("/quotes/") -def historicals(): - return API_BASE.with_path("/quotes/historicals/") +def historicals(options=False): + if options: + return API_BASE.with_path(f"/marketdata/options/historicals/") + return API_BASE.with_path(f"/quotes/historicals/") def document_requests(): From a1a2c479773ef7c277b987933e56da5893d0a072 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 01:54:40 -0700 Subject: [PATCH 03/65] streamline instrument getter functions --- pyrh/robinhood.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index b0eb7f82..c1b2d89e 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -48,6 +48,27 @@ def investment_profile(self): """Fetch investment_profile.""" return self.get(endpoints.investment_profile()) + def get_instruments(self, symbol, match=True, options=False): + """Query for instruments that match with the given ticker. + + Args: + symbol (str): stock ticker + match (bool): True if want exact match, False for partial match + + Returns: + (:obj: (list)): JSON contents from `instruments` endpoint - list + of instruments that match the ticker + """ + ticker = stock.upper() + params = {"symbol": ticker} if match else {"query": ticker} + res = self.get(endpoints.instruments(options=options), params=params) + results = res.get("results", []) + while res.get("next"): + res = res.get("next") + results.extend(res.get("results", [])) + return results + + @deprecated def instruments(self, stock): """Fetch instruments endpoint. @@ -67,6 +88,7 @@ def instruments(self, stock): return res["results"] + @deprecated def instrument(self, id): """Fetch instrument info. From 97d965a59ad64e16cc0873cdd4b65897baa86b1d Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 08:07:32 -0700 Subject: [PATCH 04/65] add popularity endpoint -make its own endpoint using instruments endpoint --- pyrh/endpoints.py | 3 +++ pyrh/robinhood.py | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyrh/endpoints.py b/pyrh/endpoints.py index 05969767..686b4fb6 100755 --- a/pyrh/endpoints.py +++ b/pyrh/endpoints.py @@ -131,3 +131,6 @@ def market_data(option_id): def convert_token(): return API_BASE.with_path("/oauth2/migrate_token/") + +def popularity(ticker): + return API_BASE.with_path(f"/instruments/{ticker}/popularity") diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index c1b2d89e..d6244c6d 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -506,9 +506,7 @@ def get_popularity(self, stock=""): """ stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] - return self.get_url(endpoints.instruments(stock_instrument, "popularity"))[ - "num_open_positions" - ] + return self.get(endpoints.popularity())["num_open_positions"] def get_tickers_by_tag(self, tag=None): """Get a list of instruments belonging to a tag From 4c62b0aef284b4fd4539eb75f718bc3066f05dba Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 20:29:46 -0700 Subject: [PATCH 05/65] remove deprecated --- pyrh/robinhood.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index d6244c6d..aa8f84cd 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -68,7 +68,6 @@ def get_instruments(self, symbol, match=True, options=False): results.extend(res.get("results", [])) return results - @deprecated def instruments(self, stock): """Fetch instruments endpoint. @@ -88,7 +87,6 @@ def instruments(self, stock): return res["results"] - @deprecated def instrument(self, id): """Fetch instrument info. From b4b5f7dda5830608458e0d6f37e63c938e8e986c Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:43:30 -0700 Subject: [PATCH 06/65] add some basic unit tests for option --- tests/test_option.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_option.py diff --git a/tests/test_option.py b/tests/test_option.py new file mode 100644 index 00000000..13fca96a --- /dev/null +++ b/tests/test_option.py @@ -0,0 +1,50 @@ +import datetime as dt +import numbers +import requests +from os import path +import pytest +from six import string_types + +from pyrh.models.option import OptionSchema + +HERE = path.abspath(path.dirname(__file__)) +ROOT = path.dirname(HERE) + +# Setup +sample_url = "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" +response = requests.get(sample_url) +data = response.json() +option_obj = OptionSchema.make_object(data=data) + + +def test_chain_symbol(): + symbol = option_obj.chain_symbol + assert isinstance(string_types, symbol) + assert symbol == "AAPL" + + +def test_strike_price(): + strike = option_obj.strike + assert isinstance(numbers.Real, strike) + assert format(strike, ".4f") == 232.5000 + + +def test_expiration_date(): + expiry = option_obj.expiration_date + assert isinstance(dt.datetime.date, expiry) + assert expiry == dt.date(2020, 4, 17) + + +def test_created_at(): + created_at = option_obj.created_at + assert isinstance(dt.datetime, created_at) + assert created_at == dt.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ") + + +def test_min_ticks(): + min_ticks = option_obj.min_ticks + assert isinstance(dict(), min_ticks) + assert min_ticks == {"above_tick": "0.05", + "below_tick": "0.01", + "cutoff_price": "3.00"} + From 0953fad44506c9d68718ebe5eb53bce991cf2642 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:43:53 -0700 Subject: [PATCH 07/65] and news/docs --- docs/stubs/pyrh.Robinhood.rst | 1 + newsfragments/201.feature | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/201.feature diff --git a/docs/stubs/pyrh.Robinhood.rst b/docs/stubs/pyrh.Robinhood.rst index 6917bfe8..88b5ac36 100644 --- a/docs/stubs/pyrh.Robinhood.rst +++ b/docs/stubs/pyrh.Robinhood.rst @@ -33,6 +33,7 @@ pyrh.Robinhood ~Robinhood.get_account ~Robinhood.get_fundamentals ~Robinhood.get_historical_quotes + ~Robinhood.get_instruments ~Robinhood.get_news ~Robinhood.get_open_orders ~Robinhood.get_option_chainid diff --git a/newsfragments/201.feature b/newsfragments/201.feature new file mode 100644 index 00000000..1dc6d864 --- /dev/null +++ b/newsfragments/201.feature @@ -0,0 +1 @@ +Add option model, refactor endpoints \ No newline at end of file From 4ab33a99c1a01085b5bc3a8591c8a126bc477c86 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:46:18 -0700 Subject: [PATCH 08/65] fix typos, requested changes --- pyrh/models/option.py | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index b1d4d454..30e95f6f 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,4 +1,4 @@ -"""Current portfolio.""" +"""Option data class""" from typing import Any @@ -16,31 +16,9 @@ class Option(BaseModel): class OptionSchema(BaseSchema): - """Robinhood Option schema data loader. - Sample result payload from - { - "chain_id": "cee01a93-626e-4ee6-9b04-60e2fd1392d1", - "chain_symbol": "AAPL", - "created_at": "2020-03-31T01:27:43.249339Z", - "expiration_date": "2020-04-17", - "id": "f098f169-74f9-4b91-b955-6834e1b67a12", - "issue_date": "2004-11-29", - "min_ticks": { - "above_tick": "0.05", - "below_tick": "0.01", - "cutoff_price": "3.00" - }, - "rhs_tradability": "untradable", - "state": "active", - "strike_price": "232.5000", - "tradability": "tradable", - "type": "put", - "updated_at": "2020-03-31T01:27:43.249354Z", - "url": "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" - } - """ + """Robinhood Option schema data loader. """ - __model__ = Options + __model__ = Option chain_id = fields.String() chain_symbol = fields.String() @@ -55,7 +33,7 @@ class OptionSchema(BaseSchema): tradability = fields.String() options_type = fields.String() updated_at = fields.NaiveDateTime() - url = fields.URL() + url = fields.URL() @post_load def make_object(self, data: JSON, **kwargs: Any) -> Option: @@ -71,10 +49,5 @@ def make_object(self, data: JSON, **kwargs: Any) -> Option: """ # Can potentially move this preprocessing part to a helper file prefix = self.__class__.__name__.lower() - reserved_word_overwrite = ["type", "id"] - for attr in reserved_word_overwrite: - k, v = f"{prefix}_{attr}", data.pop("attr") - data[k] = v - data = data.get("results", [{}])[0] return self.__model__(**data) From cf6653e8d808fca653482d9e6d168f4c3bfed9a6 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:48:29 -0700 Subject: [PATCH 09/65] fix indentation --- pyrh/robinhood.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index aa8f84cd..64a1a75c 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -59,14 +59,14 @@ def get_instruments(self, symbol, match=True, options=False): (:obj: (list)): JSON contents from `instruments` endpoint - list of instruments that match the ticker """ - ticker = stock.upper() - params = {"symbol": ticker} if match else {"query": ticker} - res = self.get(endpoints.instruments(options=options), params=params) - results = res.get("results", []) - while res.get("next"): - res = res.get("next") - results.extend(res.get("results", [])) - return results + ticker = stock.upper() + params = {"symbol": ticker} if match else {"query": ticker} + res = self.get(endpoints.instruments(options=options), params=params) + results = res.get("results", []) + while res.get("next"): + res = res.get("next") + results.extend(res.get("results", [])) + return results def instruments(self, stock): """Fetch instruments endpoint. From 8be08b3155f1da5dc7463f00c16ee3a38313bc52 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:53:08 -0700 Subject: [PATCH 10/65] fix indentation --- pyrh/robinhood.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 64a1a75c..62ebe328 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -49,16 +49,16 @@ def investment_profile(self): return self.get(endpoints.investment_profile()) def get_instruments(self, symbol, match=True, options=False): - """Query for instruments that match with the given ticker. + """Query for instruments that match with the given ticker. - Args: - symbol (str): stock ticker - match (bool): True if want exact match, False for partial match + Args: + symbol (str): stock ticker + match (bool): True if want exact match, False for partial match - Returns: - (:obj: (list)): JSON contents from `instruments` endpoint - list - of instruments that match the ticker - """ + Returns: + (:obj: (list)): JSON contents from `instruments` endpoint - list + of instruments that match the ticker + """ ticker = stock.upper() params = {"symbol": ticker} if match else {"query": ticker} res = self.get(endpoints.instruments(options=options), params=params) From b8d42efdc45ba0bc95b84ebf5475f2b5ac333e08 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 23:37:11 -0700 Subject: [PATCH 11/65] fix bugs, style --- newsfragments/201.feature | 2 +- pyrh/endpoints.py | 6 ++++-- pyrh/models/option.py | 11 +++++------ pyrh/robinhood.py | 4 ++-- tests/test_option.py | 13 ++++++++----- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/newsfragments/201.feature b/newsfragments/201.feature index 1dc6d864..31a879b4 100644 --- a/newsfragments/201.feature +++ b/newsfragments/201.feature @@ -1 +1 @@ -Add option model, refactor endpoints \ No newline at end of file +Add option model, refactor endpoints diff --git a/pyrh/endpoints.py b/pyrh/endpoints.py index 686b4fb6..e3d184c4 100755 --- a/pyrh/endpoints.py +++ b/pyrh/endpoints.py @@ -60,7 +60,8 @@ def notifications(): def orders(order_id="", options=False): opt = "/options" if options else "" - return base.with_path(f"{opt}/orders/{order_id}/") + return API_BASE.with_path(f"{opt}/orders/{order_id}/") + def password_reset(): return API_BASE.with_path("/password_reset/request/") @@ -72,7 +73,7 @@ def portfolios(): def positions(options=False): opt = "/options" if options else "" - return API_BASE.with_path("{opt}/positions/") + return API_BASE.with_path(f"{opt}/positions/") def quotes(): @@ -132,5 +133,6 @@ def market_data(option_id): def convert_token(): return API_BASE.with_path("/oauth2/migrate_token/") + def popularity(ticker): return API_BASE.with_path(f"/instruments/{ticker}/popularity") diff --git a/pyrh/models/option.py b/pyrh/models/option.py index 30e95f6f..e67e4538 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,4 +1,4 @@ -"""Option data class""" +"""Option data class.""" from typing import Any @@ -10,13 +10,13 @@ class Option(BaseModel): - """Robinhood Option data class. Represents an options instrument""" + """Robinhood Option data class. Represents an options instrument.""" pass class OptionSchema(BaseSchema): - """Robinhood Option schema data loader. """ + """Robinhood Option schema data loader.""" __model__ = Option @@ -24,14 +24,14 @@ class OptionSchema(BaseSchema): chain_symbol = fields.String() created_at = fields.NaiveDateTime() expiration_date = fields.Date() - options_id = fields.String() + id = fields.String() issue_date = fields.Date() min_ticks = fields.Dict() rhs_tradability = fields.String() state = fields.String() strike_price = fields.Float() tradability = fields.String() - options_type = fields.String() + type = fields.String() updated_at = fields.NaiveDateTime() url = fields.URL() @@ -48,6 +48,5 @@ def make_object(self, data: JSON, **kwargs: Any) -> Option: """ # Can potentially move this preprocessing part to a helper file - prefix = self.__class__.__name__.lower() data = data.get("results", [{}])[0] return self.__model__(**data) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 62ebe328..7fa6f7ce 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -59,7 +59,7 @@ def get_instruments(self, symbol, match=True, options=False): (:obj: (list)): JSON contents from `instruments` endpoint - list of instruments that match the ticker """ - ticker = stock.upper() + ticker = symbol.upper() params = {"symbol": ticker} if match else {"query": ticker} res = self.get(endpoints.instruments(options=options), params=params) results = res.get("results", []) @@ -504,7 +504,7 @@ def get_popularity(self, stock=""): """ stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] - return self.get(endpoints.popularity())["num_open_positions"] + return self.get(endpoints.popularity(stock_instrument))["num_open_positions"] def get_tickers_by_tag(self, tag=None): """Get a list of instruments belonging to a tag diff --git a/tests/test_option.py b/tests/test_option.py index 13fca96a..d9503d16 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -1,12 +1,14 @@ import datetime as dt import numbers -import requests from os import path + import pytest +import requests from six import string_types from pyrh.models.option import OptionSchema + HERE = path.abspath(path.dirname(__file__)) ROOT = path.dirname(HERE) @@ -44,7 +46,8 @@ def test_created_at(): def test_min_ticks(): min_ticks = option_obj.min_ticks assert isinstance(dict(), min_ticks) - assert min_ticks == {"above_tick": "0.05", - "below_tick": "0.01", - "cutoff_price": "3.00"} - + assert min_ticks == { + "above_tick": "0.05", + "below_tick": "0.01", + "cutoff_price": "3.00", + } From 346bc61fb647747f92dc40799c38e3458d255214 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 23:38:46 -0700 Subject: [PATCH 12/65] update known_third_party --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9157b66d..f31369ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ exclude = ''' [tool.isort] known_first_party = 'robinhood' -known_third_party = ["certifi", "dateutil", "freezegun", "marshmallow", "pytest", "pytz", "requests", "requests_mock", "yarl"] +known_third_party = ["certifi", "dateutil", "freezegun", "marshmallow", "pytest", "pytz", "requests", "requests_mock", "six", "yarl"] multi_line_output = 3 lines_after_imports = 2 force_grid_wrap = 0 From b6eae0c63372d03dc933d6f993829ed34c4b3de7 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 23:52:38 -0700 Subject: [PATCH 13/65] fix OptionSchema init --- tests/test_option.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_option.py b/tests/test_option.py index d9503d16..dbb42d94 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -16,7 +16,8 @@ sample_url = "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" response = requests.get(sample_url) data = response.json() -option_obj = OptionSchema.make_object(data=data) +option_schema = OptionSchema() +option_obj = option_schema.make_object(data=data) def test_chain_symbol(): From 87909ae4fe4b0011ec0e7b85ed3d8ce0f2427945 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 15 Apr 2020 00:04:59 -0700 Subject: [PATCH 14/65] fix formatting of data in option --- pyrh/models/option.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index e67e4538..244db4a7 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -47,6 +47,4 @@ def make_object(self, data: JSON, **kwargs: Any) -> Option: An instance of the Option class. """ - # Can potentially move this preprocessing part to a helper file - data = data.get("results", [{}])[0] return self.__model__(**data) From 0facead8808e9d01d569f59c286fac8ad4dbcca7 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 15 Apr 2020 00:22:44 -0700 Subject: [PATCH 15/65] fix order of isinstance args --- tests/test_option.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_option.py b/tests/test_option.py index dbb42d94..7bcba5dc 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -22,31 +22,31 @@ def test_chain_symbol(): symbol = option_obj.chain_symbol - assert isinstance(string_types, symbol) + assert isinstance(symbol, string_types) assert symbol == "AAPL" def test_strike_price(): strike = option_obj.strike - assert isinstance(numbers.Real, strike) + assert isinstance(strike, numbers.Real) assert format(strike, ".4f") == 232.5000 def test_expiration_date(): expiry = option_obj.expiration_date - assert isinstance(dt.datetime.date, expiry) + assert isinstance(expiry, dt.datetime.date) assert expiry == dt.date(2020, 4, 17) def test_created_at(): created_at = option_obj.created_at - assert isinstance(dt.datetime, created_at) + assert isinstance(created_at, dt.datetime) assert created_at == dt.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ") def test_min_ticks(): min_ticks = option_obj.min_ticks - assert isinstance(dict(), min_ticks) + assert isinstance(min_ticks, dict()) assert min_ticks == { "above_tick": "0.05", "below_tick": "0.01", From c5538b0071b8364f942849b39117e31d84ca139c Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 15 Apr 2020 00:31:26 -0700 Subject: [PATCH 16/65] fix attr name --- tests/test_option.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_option.py b/tests/test_option.py index 7bcba5dc..28cb0635 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -27,7 +27,7 @@ def test_chain_symbol(): def test_strike_price(): - strike = option_obj.strike + strike = option_obj.strike_price assert isinstance(strike, numbers.Real) assert format(strike, ".4f") == 232.5000 From 5e130c1b1c96e990eaeeb453b7f59a0f325539f8 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 18 Apr 2020 00:00:32 -0700 Subject: [PATCH 17/65] fix option model, tests --- .pre-commit-config.yaml | 1 - pyrh/models/option.py | 36 +++++++++++++++++++++++++++++++++--- tests/test_option.py | 22 ++++++++++------------ 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8259dc39..7a51a828 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,6 @@ repos: rev: 19.10b0 hooks: - id: black - language_version: python3.6 - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.9 hooks: diff --git a/pyrh/models/option.py b/pyrh/models/option.py index 244db4a7..d846b56f 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -9,6 +9,36 @@ from .base import BaseModel, BaseSchema +class MinTicks(BaseModel): + """Min ticks data class. Describes min increments the option can be traded at.""" + + pass + + +class MinTicksSchema(BaseSchema): + """Min ticks schema data loader.""" + + __model__ = MinTicks + + above_tick = fields.Float() + below_tick = fields.Float() + cutoff_price = fields.Float() + + @post_load + def make_object(self, data: JSON, **kwargs: Any) -> MinTicks: + """Build model for the Min ticks class. + + Args: + data: The JSON diction to use to build the Min tick. + **kwargs: Unused but required to match signature of `Schema.make_object` + + Returns: + An instance of the Min tick class. + + """ + return self.__model__(**data) + + class Option(BaseModel): """Robinhood Option data class. Represents an options instrument.""" @@ -22,17 +52,17 @@ class OptionSchema(BaseSchema): chain_id = fields.String() chain_symbol = fields.String() - created_at = fields.NaiveDateTime() + created_at = fields.DateTime() expiration_date = fields.Date() id = fields.String() issue_date = fields.Date() - min_ticks = fields.Dict() + min_ticks = fields.Nested(MinTicksSchema) rhs_tradability = fields.String() state = fields.String() strike_price = fields.Float() tradability = fields.String() type = fields.String() - updated_at = fields.NaiveDateTime() + updated_at = fields.DateTime() url = fields.URL() @post_load diff --git a/tests/test_option.py b/tests/test_option.py index 28cb0635..f73730ca 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -2,11 +2,11 @@ import numbers from os import path -import pytest import requests +from dateutil import parser as p from six import string_types -from pyrh.models.option import OptionSchema +from pyrh.models.option import MinTicks, OptionSchema HERE = path.abspath(path.dirname(__file__)) @@ -17,7 +17,7 @@ response = requests.get(sample_url) data = response.json() option_schema = OptionSchema() -option_obj = option_schema.make_object(data=data) +option_obj = option_schema.load(data) def test_chain_symbol(): @@ -29,26 +29,24 @@ def test_chain_symbol(): def test_strike_price(): strike = option_obj.strike_price assert isinstance(strike, numbers.Real) - assert format(strike, ".4f") == 232.5000 + assert strike == 232.5 def test_expiration_date(): expiry = option_obj.expiration_date - assert isinstance(expiry, dt.datetime.date) + assert isinstance(expiry, dt.date) assert expiry == dt.date(2020, 4, 17) def test_created_at(): created_at = option_obj.created_at assert isinstance(created_at, dt.datetime) - assert created_at == dt.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ") + assert created_at == p.parse("2020-03-31T01:27:43.249339Z") def test_min_ticks(): min_ticks = option_obj.min_ticks - assert isinstance(min_ticks, dict()) - assert min_ticks == { - "above_tick": "0.05", - "below_tick": "0.01", - "cutoff_price": "3.00", - } + assert isinstance(min_ticks, MinTicks) + assert min_ticks.above_tick == 0.05 + assert min_ticks.below_tick == 0.01 + assert min_ticks.cutoff_price == 3.00 From 2075429284faf21bc66b4cc210a7f7b3baf86688 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 18 Apr 2020 00:10:13 -0700 Subject: [PATCH 18/65] remove redundant code --- pyrh/models/option.py | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index d846b56f..f26ce6f2 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,10 +1,6 @@ """Option data class.""" -from typing import Any - -from marshmallow import fields, post_load - -from pyrh.common import JSON +from marshmallow import fields from .base import BaseModel, BaseSchema @@ -24,20 +20,6 @@ class MinTicksSchema(BaseSchema): below_tick = fields.Float() cutoff_price = fields.Float() - @post_load - def make_object(self, data: JSON, **kwargs: Any) -> MinTicks: - """Build model for the Min ticks class. - - Args: - data: The JSON diction to use to build the Min tick. - **kwargs: Unused but required to match signature of `Schema.make_object` - - Returns: - An instance of the Min tick class. - - """ - return self.__model__(**data) - class Option(BaseModel): """Robinhood Option data class. Represents an options instrument.""" @@ -64,17 +46,3 @@ class OptionSchema(BaseSchema): type = fields.String() updated_at = fields.DateTime() url = fields.URL() - - @post_load - def make_object(self, data: JSON, **kwargs: Any) -> Option: - """Build model for the Option class. - - Args: - data: The JSON diction to use to build the Option. - **kwargs: Unused but required to match signature of `Schema.make_object` - - Returns: - An instance of the Option class. - - """ - return self.__model__(**data) From cae9060aefa8fdc6ef4f3e1efed9e8795c4445aa Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 01:44:38 -0700 Subject: [PATCH 19/65] create a model for an option instrument --- pyrh/models/option.py | 80 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 pyrh/models/option.py diff --git a/pyrh/models/option.py b/pyrh/models/option.py new file mode 100644 index 00000000..b1d4d454 --- /dev/null +++ b/pyrh/models/option.py @@ -0,0 +1,80 @@ +"""Current portfolio.""" + +from typing import Any + +from marshmallow import fields, post_load + +from pyrh.common import JSON + +from .base import BaseModel, BaseSchema + + +class Option(BaseModel): + """Robinhood Option data class. Represents an options instrument""" + + pass + + +class OptionSchema(BaseSchema): + """Robinhood Option schema data loader. + Sample result payload from + { + "chain_id": "cee01a93-626e-4ee6-9b04-60e2fd1392d1", + "chain_symbol": "AAPL", + "created_at": "2020-03-31T01:27:43.249339Z", + "expiration_date": "2020-04-17", + "id": "f098f169-74f9-4b91-b955-6834e1b67a12", + "issue_date": "2004-11-29", + "min_ticks": { + "above_tick": "0.05", + "below_tick": "0.01", + "cutoff_price": "3.00" + }, + "rhs_tradability": "untradable", + "state": "active", + "strike_price": "232.5000", + "tradability": "tradable", + "type": "put", + "updated_at": "2020-03-31T01:27:43.249354Z", + "url": "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" + } + """ + + __model__ = Options + + chain_id = fields.String() + chain_symbol = fields.String() + created_at = fields.NaiveDateTime() + expiration_date = fields.Date() + options_id = fields.String() + issue_date = fields.Date() + min_ticks = fields.Dict() + rhs_tradability = fields.String() + state = fields.String() + strike_price = fields.Float() + tradability = fields.String() + options_type = fields.String() + updated_at = fields.NaiveDateTime() + url = fields.URL() + + @post_load + def make_object(self, data: JSON, **kwargs: Any) -> Option: + """Build model for the Option class. + + Args: + data: The JSON diction to use to build the Option. + **kwargs: Unused but required to match signature of `Schema.make_object` + + Returns: + An instance of the Option class. + + """ + # Can potentially move this preprocessing part to a helper file + prefix = self.__class__.__name__.lower() + reserved_word_overwrite = ["type", "id"] + for attr in reserved_word_overwrite: + k, v = f"{prefix}_{attr}", data.pop("attr") + data[k] = v + + data = data.get("results", [{}])[0] + return self.__model__(**data) From dec0d738de31414c0e279cc635bc6a632a6b3ca3 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 01:54:40 -0700 Subject: [PATCH 20/65] streamline instrument getter functions --- pyrh/robinhood.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 3d1d7792..97abd49b 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -46,6 +46,27 @@ def investment_profile(self): """Fetch investment_profile.""" return self.get(urls.INVESTMENT_PROFILE) + def get_instruments(self, symbol, match=True, options=False): + """Query for instruments that match with the given ticker. + + Args: + symbol (str): stock ticker + match (bool): True if want exact match, False for partial match + + Returns: + (:obj: (list)): JSON contents from `instruments` endpoint - list + of instruments that match the ticker + """ + ticker = stock.upper() + params = {"symbol": ticker} if match else {"query": ticker} + res = self.get(endpoints.instruments(options=options), params=params) + results = res.get("results", []) + while res.get("next"): + res = res.get("next") + results.extend(res.get("results", [])) + return results + + @deprecated def instruments(self, stock): """Fetch instruments endpoint. @@ -65,6 +86,7 @@ def instruments(self, stock): return res["results"] + @deprecated def instrument(self, id): """Fetch instrument info. From b0b55fa7dbdc596a88495cb8cdf62df1a96e3459 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 20:29:46 -0700 Subject: [PATCH 21/65] remove deprecated --- pyrh/robinhood.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 97abd49b..77245bfe 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -66,7 +66,6 @@ def get_instruments(self, symbol, match=True, options=False): results.extend(res.get("results", [])) return results - @deprecated def instruments(self, stock): """Fetch instruments endpoint. @@ -86,7 +85,6 @@ def instruments(self, stock): return res["results"] - @deprecated def instrument(self, id): """Fetch instrument info. From 109ef496c455cda82c1c0df6afc2ae1d601a80b0 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:43:30 -0700 Subject: [PATCH 22/65] add some basic unit tests for option --- tests/test_option.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_option.py diff --git a/tests/test_option.py b/tests/test_option.py new file mode 100644 index 00000000..13fca96a --- /dev/null +++ b/tests/test_option.py @@ -0,0 +1,50 @@ +import datetime as dt +import numbers +import requests +from os import path +import pytest +from six import string_types + +from pyrh.models.option import OptionSchema + +HERE = path.abspath(path.dirname(__file__)) +ROOT = path.dirname(HERE) + +# Setup +sample_url = "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" +response = requests.get(sample_url) +data = response.json() +option_obj = OptionSchema.make_object(data=data) + + +def test_chain_symbol(): + symbol = option_obj.chain_symbol + assert isinstance(string_types, symbol) + assert symbol == "AAPL" + + +def test_strike_price(): + strike = option_obj.strike + assert isinstance(numbers.Real, strike) + assert format(strike, ".4f") == 232.5000 + + +def test_expiration_date(): + expiry = option_obj.expiration_date + assert isinstance(dt.datetime.date, expiry) + assert expiry == dt.date(2020, 4, 17) + + +def test_created_at(): + created_at = option_obj.created_at + assert isinstance(dt.datetime, created_at) + assert created_at == dt.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ") + + +def test_min_ticks(): + min_ticks = option_obj.min_ticks + assert isinstance(dict(), min_ticks) + assert min_ticks == {"above_tick": "0.05", + "below_tick": "0.01", + "cutoff_price": "3.00"} + From fd583efa2d9c6bef606a55de97d4b2d91d601131 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:43:53 -0700 Subject: [PATCH 23/65] and news/docs --- docs/stubs/pyrh.Robinhood.rst | 1 + newsfragments/201.feature | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/201.feature diff --git a/docs/stubs/pyrh.Robinhood.rst b/docs/stubs/pyrh.Robinhood.rst index 6917bfe8..88b5ac36 100644 --- a/docs/stubs/pyrh.Robinhood.rst +++ b/docs/stubs/pyrh.Robinhood.rst @@ -33,6 +33,7 @@ pyrh.Robinhood ~Robinhood.get_account ~Robinhood.get_fundamentals ~Robinhood.get_historical_quotes + ~Robinhood.get_instruments ~Robinhood.get_news ~Robinhood.get_open_orders ~Robinhood.get_option_chainid diff --git a/newsfragments/201.feature b/newsfragments/201.feature new file mode 100644 index 00000000..1dc6d864 --- /dev/null +++ b/newsfragments/201.feature @@ -0,0 +1 @@ +Add option model, refactor endpoints \ No newline at end of file From fa8d01f5ce7fbd0dbe0b1ef687c86484ef499c8f Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:46:18 -0700 Subject: [PATCH 24/65] fix typos, requested changes --- pyrh/models/option.py | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index b1d4d454..30e95f6f 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,4 +1,4 @@ -"""Current portfolio.""" +"""Option data class""" from typing import Any @@ -16,31 +16,9 @@ class Option(BaseModel): class OptionSchema(BaseSchema): - """Robinhood Option schema data loader. - Sample result payload from - { - "chain_id": "cee01a93-626e-4ee6-9b04-60e2fd1392d1", - "chain_symbol": "AAPL", - "created_at": "2020-03-31T01:27:43.249339Z", - "expiration_date": "2020-04-17", - "id": "f098f169-74f9-4b91-b955-6834e1b67a12", - "issue_date": "2004-11-29", - "min_ticks": { - "above_tick": "0.05", - "below_tick": "0.01", - "cutoff_price": "3.00" - }, - "rhs_tradability": "untradable", - "state": "active", - "strike_price": "232.5000", - "tradability": "tradable", - "type": "put", - "updated_at": "2020-03-31T01:27:43.249354Z", - "url": "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" - } - """ + """Robinhood Option schema data loader. """ - __model__ = Options + __model__ = Option chain_id = fields.String() chain_symbol = fields.String() @@ -55,7 +33,7 @@ class OptionSchema(BaseSchema): tradability = fields.String() options_type = fields.String() updated_at = fields.NaiveDateTime() - url = fields.URL() + url = fields.URL() @post_load def make_object(self, data: JSON, **kwargs: Any) -> Option: @@ -71,10 +49,5 @@ def make_object(self, data: JSON, **kwargs: Any) -> Option: """ # Can potentially move this preprocessing part to a helper file prefix = self.__class__.__name__.lower() - reserved_word_overwrite = ["type", "id"] - for attr in reserved_word_overwrite: - k, v = f"{prefix}_{attr}", data.pop("attr") - data[k] = v - data = data.get("results", [{}])[0] return self.__model__(**data) From 0e14bb27d1b6680dea2eba9ec5f4e1b9a305e346 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:48:29 -0700 Subject: [PATCH 25/65] fix indentation --- pyrh/robinhood.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 77245bfe..492a09ce 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -57,14 +57,14 @@ def get_instruments(self, symbol, match=True, options=False): (:obj: (list)): JSON contents from `instruments` endpoint - list of instruments that match the ticker """ - ticker = stock.upper() - params = {"symbol": ticker} if match else {"query": ticker} - res = self.get(endpoints.instruments(options=options), params=params) - results = res.get("results", []) - while res.get("next"): - res = res.get("next") - results.extend(res.get("results", [])) - return results + ticker = stock.upper() + params = {"symbol": ticker} if match else {"query": ticker} + res = self.get(endpoints.instruments(options=options), params=params) + results = res.get("results", []) + while res.get("next"): + res = res.get("next") + results.extend(res.get("results", [])) + return results def instruments(self, stock): """Fetch instruments endpoint. From 508a57de65f379828288f632053ecb82645d5bad Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:53:08 -0700 Subject: [PATCH 26/65] fix indentation --- pyrh/robinhood.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 492a09ce..49963134 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -47,16 +47,16 @@ def investment_profile(self): return self.get(urls.INVESTMENT_PROFILE) def get_instruments(self, symbol, match=True, options=False): - """Query for instruments that match with the given ticker. + """Query for instruments that match with the given ticker. - Args: - symbol (str): stock ticker - match (bool): True if want exact match, False for partial match + Args: + symbol (str): stock ticker + match (bool): True if want exact match, False for partial match - Returns: - (:obj: (list)): JSON contents from `instruments` endpoint - list - of instruments that match the ticker - """ + Returns: + (:obj: (list)): JSON contents from `instruments` endpoint - list + of instruments that match the ticker + """ ticker = stock.upper() params = {"symbol": ticker} if match else {"query": ticker} res = self.get(endpoints.instruments(options=options), params=params) From 556fc99957a7393af7f088eec2e0c663d2486dae Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 23:37:11 -0700 Subject: [PATCH 27/65] fix bugs, style --- newsfragments/201.feature | 2 +- pyrh/models/option.py | 11 +++++------ pyrh/robinhood.py | 6 ++---- tests/test_option.py | 13 ++++++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/newsfragments/201.feature b/newsfragments/201.feature index 1dc6d864..31a879b4 100644 --- a/newsfragments/201.feature +++ b/newsfragments/201.feature @@ -1 +1 @@ -Add option model, refactor endpoints \ No newline at end of file +Add option model, refactor endpoints diff --git a/pyrh/models/option.py b/pyrh/models/option.py index 30e95f6f..e67e4538 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,4 +1,4 @@ -"""Option data class""" +"""Option data class.""" from typing import Any @@ -10,13 +10,13 @@ class Option(BaseModel): - """Robinhood Option data class. Represents an options instrument""" + """Robinhood Option data class. Represents an options instrument.""" pass class OptionSchema(BaseSchema): - """Robinhood Option schema data loader. """ + """Robinhood Option schema data loader.""" __model__ = Option @@ -24,14 +24,14 @@ class OptionSchema(BaseSchema): chain_symbol = fields.String() created_at = fields.NaiveDateTime() expiration_date = fields.Date() - options_id = fields.String() + id = fields.String() issue_date = fields.Date() min_ticks = fields.Dict() rhs_tradability = fields.String() state = fields.String() strike_price = fields.Float() tradability = fields.String() - options_type = fields.String() + type = fields.String() updated_at = fields.NaiveDateTime() url = fields.URL() @@ -48,6 +48,5 @@ def make_object(self, data: JSON, **kwargs: Any) -> Option: """ # Can potentially move this preprocessing part to a helper file - prefix = self.__class__.__name__.lower() data = data.get("results", [{}])[0] return self.__model__(**data) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 49963134..ebd34548 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -57,7 +57,7 @@ def get_instruments(self, symbol, match=True, options=False): (:obj: (list)): JSON contents from `instruments` endpoint - list of instruments that match the ticker """ - ticker = stock.upper() + ticker = symbol.upper() params = {"symbol": ticker} if match else {"query": ticker} res = self.get(endpoints.instruments(options=options), params=params) results = res.get("results", []) @@ -502,9 +502,7 @@ def get_popularity(self, stock=""): """ stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] - return self.get_url(urls.build_instruments(stock_instrument, "popularity"))[ - "num_open_positions" - ] + return self.get(endpoints.popularity(stock_instrument))["num_open_positions"] def get_tickers_by_tag(self, tag=None): """Get a list of instruments belonging to a tag diff --git a/tests/test_option.py b/tests/test_option.py index 13fca96a..d9503d16 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -1,12 +1,14 @@ import datetime as dt import numbers -import requests from os import path + import pytest +import requests from six import string_types from pyrh.models.option import OptionSchema + HERE = path.abspath(path.dirname(__file__)) ROOT = path.dirname(HERE) @@ -44,7 +46,8 @@ def test_created_at(): def test_min_ticks(): min_ticks = option_obj.min_ticks assert isinstance(dict(), min_ticks) - assert min_ticks == {"above_tick": "0.05", - "below_tick": "0.01", - "cutoff_price": "3.00"} - + assert min_ticks == { + "above_tick": "0.05", + "below_tick": "0.01", + "cutoff_price": "3.00", + } From bea0f9d8854d710d8d7151718e5554da0fb95a7d Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 23:38:46 -0700 Subject: [PATCH 28/65] update known_third_party --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9157b66d..f31369ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ exclude = ''' [tool.isort] known_first_party = 'robinhood' -known_third_party = ["certifi", "dateutil", "freezegun", "marshmallow", "pytest", "pytz", "requests", "requests_mock", "yarl"] +known_third_party = ["certifi", "dateutil", "freezegun", "marshmallow", "pytest", "pytz", "requests", "requests_mock", "six", "yarl"] multi_line_output = 3 lines_after_imports = 2 force_grid_wrap = 0 From 24294c869517bb5fa7a575494c8da1d611ac9877 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 23:52:38 -0700 Subject: [PATCH 29/65] fix OptionSchema init --- tests/test_option.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_option.py b/tests/test_option.py index d9503d16..dbb42d94 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -16,7 +16,8 @@ sample_url = "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" response = requests.get(sample_url) data = response.json() -option_obj = OptionSchema.make_object(data=data) +option_schema = OptionSchema() +option_obj = option_schema.make_object(data=data) def test_chain_symbol(): From 18017fdb9335b0163a35f75a87575d0e6ecd2100 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 15 Apr 2020 00:04:59 -0700 Subject: [PATCH 30/65] fix formatting of data in option --- pyrh/models/option.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index e67e4538..244db4a7 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -47,6 +47,4 @@ def make_object(self, data: JSON, **kwargs: Any) -> Option: An instance of the Option class. """ - # Can potentially move this preprocessing part to a helper file - data = data.get("results", [{}])[0] return self.__model__(**data) From f4e904432fbefa10b7490e0aed793401d9feb83c Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 15 Apr 2020 00:22:44 -0700 Subject: [PATCH 31/65] fix order of isinstance args --- tests/test_option.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_option.py b/tests/test_option.py index dbb42d94..7bcba5dc 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -22,31 +22,31 @@ def test_chain_symbol(): symbol = option_obj.chain_symbol - assert isinstance(string_types, symbol) + assert isinstance(symbol, string_types) assert symbol == "AAPL" def test_strike_price(): strike = option_obj.strike - assert isinstance(numbers.Real, strike) + assert isinstance(strike, numbers.Real) assert format(strike, ".4f") == 232.5000 def test_expiration_date(): expiry = option_obj.expiration_date - assert isinstance(dt.datetime.date, expiry) + assert isinstance(expiry, dt.datetime.date) assert expiry == dt.date(2020, 4, 17) def test_created_at(): created_at = option_obj.created_at - assert isinstance(dt.datetime, created_at) + assert isinstance(created_at, dt.datetime) assert created_at == dt.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ") def test_min_ticks(): min_ticks = option_obj.min_ticks - assert isinstance(dict(), min_ticks) + assert isinstance(min_ticks, dict()) assert min_ticks == { "above_tick": "0.05", "below_tick": "0.01", From 2c391de3c8cb5acbf56841b4b6ed48ca5628358c Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 15 Apr 2020 00:31:26 -0700 Subject: [PATCH 32/65] fix attr name --- tests/test_option.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_option.py b/tests/test_option.py index 7bcba5dc..28cb0635 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -27,7 +27,7 @@ def test_chain_symbol(): def test_strike_price(): - strike = option_obj.strike + strike = option_obj.strike_price assert isinstance(strike, numbers.Real) assert format(strike, ".4f") == 232.5000 From 30898f4d574af3062fbc49b017e5c168ff94b8ee Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 18 Apr 2020 00:00:32 -0700 Subject: [PATCH 33/65] fix option model, tests --- pyrh/models/option.py | 36 +++++++++++++++++++++++++++++++++--- tests/test_option.py | 22 ++++++++++------------ 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index 244db4a7..d846b56f 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -9,6 +9,36 @@ from .base import BaseModel, BaseSchema +class MinTicks(BaseModel): + """Min ticks data class. Describes min increments the option can be traded at.""" + + pass + + +class MinTicksSchema(BaseSchema): + """Min ticks schema data loader.""" + + __model__ = MinTicks + + above_tick = fields.Float() + below_tick = fields.Float() + cutoff_price = fields.Float() + + @post_load + def make_object(self, data: JSON, **kwargs: Any) -> MinTicks: + """Build model for the Min ticks class. + + Args: + data: The JSON diction to use to build the Min tick. + **kwargs: Unused but required to match signature of `Schema.make_object` + + Returns: + An instance of the Min tick class. + + """ + return self.__model__(**data) + + class Option(BaseModel): """Robinhood Option data class. Represents an options instrument.""" @@ -22,17 +52,17 @@ class OptionSchema(BaseSchema): chain_id = fields.String() chain_symbol = fields.String() - created_at = fields.NaiveDateTime() + created_at = fields.DateTime() expiration_date = fields.Date() id = fields.String() issue_date = fields.Date() - min_ticks = fields.Dict() + min_ticks = fields.Nested(MinTicksSchema) rhs_tradability = fields.String() state = fields.String() strike_price = fields.Float() tradability = fields.String() type = fields.String() - updated_at = fields.NaiveDateTime() + updated_at = fields.DateTime() url = fields.URL() @post_load diff --git a/tests/test_option.py b/tests/test_option.py index 28cb0635..f73730ca 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -2,11 +2,11 @@ import numbers from os import path -import pytest import requests +from dateutil import parser as p from six import string_types -from pyrh.models.option import OptionSchema +from pyrh.models.option import MinTicks, OptionSchema HERE = path.abspath(path.dirname(__file__)) @@ -17,7 +17,7 @@ response = requests.get(sample_url) data = response.json() option_schema = OptionSchema() -option_obj = option_schema.make_object(data=data) +option_obj = option_schema.load(data) def test_chain_symbol(): @@ -29,26 +29,24 @@ def test_chain_symbol(): def test_strike_price(): strike = option_obj.strike_price assert isinstance(strike, numbers.Real) - assert format(strike, ".4f") == 232.5000 + assert strike == 232.5 def test_expiration_date(): expiry = option_obj.expiration_date - assert isinstance(expiry, dt.datetime.date) + assert isinstance(expiry, dt.date) assert expiry == dt.date(2020, 4, 17) def test_created_at(): created_at = option_obj.created_at assert isinstance(created_at, dt.datetime) - assert created_at == dt.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ") + assert created_at == p.parse("2020-03-31T01:27:43.249339Z") def test_min_ticks(): min_ticks = option_obj.min_ticks - assert isinstance(min_ticks, dict()) - assert min_ticks == { - "above_tick": "0.05", - "below_tick": "0.01", - "cutoff_price": "3.00", - } + assert isinstance(min_ticks, MinTicks) + assert min_ticks.above_tick == 0.05 + assert min_ticks.below_tick == 0.01 + assert min_ticks.cutoff_price == 3.00 From 41313cda7c560558da8cc6b7c8efaa5af5d850ea Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 18 Apr 2020 00:10:13 -0700 Subject: [PATCH 34/65] remove redundant code --- pyrh/models/option.py | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index d846b56f..f26ce6f2 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,10 +1,6 @@ """Option data class.""" -from typing import Any - -from marshmallow import fields, post_load - -from pyrh.common import JSON +from marshmallow import fields from .base import BaseModel, BaseSchema @@ -24,20 +20,6 @@ class MinTicksSchema(BaseSchema): below_tick = fields.Float() cutoff_price = fields.Float() - @post_load - def make_object(self, data: JSON, **kwargs: Any) -> MinTicks: - """Build model for the Min ticks class. - - Args: - data: The JSON diction to use to build the Min tick. - **kwargs: Unused but required to match signature of `Schema.make_object` - - Returns: - An instance of the Min tick class. - - """ - return self.__model__(**data) - class Option(BaseModel): """Robinhood Option data class. Represents an options instrument.""" @@ -64,17 +46,3 @@ class OptionSchema(BaseSchema): type = fields.String() updated_at = fields.DateTime() url = fields.URL() - - @post_load - def make_object(self, data: JSON, **kwargs: Any) -> Option: - """Build model for the Option class. - - Args: - data: The JSON diction to use to build the Option. - **kwargs: Unused but required to match signature of `Schema.make_object` - - Returns: - An instance of the Option class. - - """ - return self.__model__(**data) From a6320532e544623a588adee7d6e341740804bc8e Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 18 Apr 2020 13:45:24 -0700 Subject: [PATCH 35/65] update endpoints to urls --- pyrh/robinhood.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index ebd34548..f7900325 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -59,7 +59,7 @@ def get_instruments(self, symbol, match=True, options=False): """ ticker = symbol.upper() params = {"symbol": ticker} if match else {"query": ticker} - res = self.get(endpoints.instruments(options=options), params=params) + res = self.get(urls.instruments(options=options), params=params) results = res.get("results", []) while res.get("next"): res = res.get("next") @@ -491,19 +491,6 @@ def get_url(self, url): return self.get(url) - def get_popularity(self, stock=""): - """Get the number of robinhood users who own the given stock - - Args: - stock (str): stock ticker - - Returns: - (int): number of users who own the stock - - """ - stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] - return self.get(endpoints.popularity(stock_instrument))["num_open_positions"] - def get_tickers_by_tag(self, tag=None): """Get a list of instruments belonging to a tag From 57d5e3f44a770b394004fa51239f6e3a81a7a676 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 01:44:38 -0700 Subject: [PATCH 36/65] create a model for an option instrument --- pyrh/models/option.py | 80 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 pyrh/models/option.py diff --git a/pyrh/models/option.py b/pyrh/models/option.py new file mode 100644 index 00000000..b1d4d454 --- /dev/null +++ b/pyrh/models/option.py @@ -0,0 +1,80 @@ +"""Current portfolio.""" + +from typing import Any + +from marshmallow import fields, post_load + +from pyrh.common import JSON + +from .base import BaseModel, BaseSchema + + +class Option(BaseModel): + """Robinhood Option data class. Represents an options instrument""" + + pass + + +class OptionSchema(BaseSchema): + """Robinhood Option schema data loader. + Sample result payload from + { + "chain_id": "cee01a93-626e-4ee6-9b04-60e2fd1392d1", + "chain_symbol": "AAPL", + "created_at": "2020-03-31T01:27:43.249339Z", + "expiration_date": "2020-04-17", + "id": "f098f169-74f9-4b91-b955-6834e1b67a12", + "issue_date": "2004-11-29", + "min_ticks": { + "above_tick": "0.05", + "below_tick": "0.01", + "cutoff_price": "3.00" + }, + "rhs_tradability": "untradable", + "state": "active", + "strike_price": "232.5000", + "tradability": "tradable", + "type": "put", + "updated_at": "2020-03-31T01:27:43.249354Z", + "url": "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" + } + """ + + __model__ = Options + + chain_id = fields.String() + chain_symbol = fields.String() + created_at = fields.NaiveDateTime() + expiration_date = fields.Date() + options_id = fields.String() + issue_date = fields.Date() + min_ticks = fields.Dict() + rhs_tradability = fields.String() + state = fields.String() + strike_price = fields.Float() + tradability = fields.String() + options_type = fields.String() + updated_at = fields.NaiveDateTime() + url = fields.URL() + + @post_load + def make_object(self, data: JSON, **kwargs: Any) -> Option: + """Build model for the Option class. + + Args: + data: The JSON diction to use to build the Option. + **kwargs: Unused but required to match signature of `Schema.make_object` + + Returns: + An instance of the Option class. + + """ + # Can potentially move this preprocessing part to a helper file + prefix = self.__class__.__name__.lower() + reserved_word_overwrite = ["type", "id"] + for attr in reserved_word_overwrite: + k, v = f"{prefix}_{attr}", data.pop("attr") + data[k] = v + + data = data.get("results", [{}])[0] + return self.__model__(**data) From f39b124d217a7fdb90f5bea20c5434a7e8a9257d Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:43:30 -0700 Subject: [PATCH 37/65] add some basic unit tests for option --- tests/test_option.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_option.py diff --git a/tests/test_option.py b/tests/test_option.py new file mode 100644 index 00000000..13fca96a --- /dev/null +++ b/tests/test_option.py @@ -0,0 +1,50 @@ +import datetime as dt +import numbers +import requests +from os import path +import pytest +from six import string_types + +from pyrh.models.option import OptionSchema + +HERE = path.abspath(path.dirname(__file__)) +ROOT = path.dirname(HERE) + +# Setup +sample_url = "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" +response = requests.get(sample_url) +data = response.json() +option_obj = OptionSchema.make_object(data=data) + + +def test_chain_symbol(): + symbol = option_obj.chain_symbol + assert isinstance(string_types, symbol) + assert symbol == "AAPL" + + +def test_strike_price(): + strike = option_obj.strike + assert isinstance(numbers.Real, strike) + assert format(strike, ".4f") == 232.5000 + + +def test_expiration_date(): + expiry = option_obj.expiration_date + assert isinstance(dt.datetime.date, expiry) + assert expiry == dt.date(2020, 4, 17) + + +def test_created_at(): + created_at = option_obj.created_at + assert isinstance(dt.datetime, created_at) + assert created_at == dt.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ") + + +def test_min_ticks(): + min_ticks = option_obj.min_ticks + assert isinstance(dict(), min_ticks) + assert min_ticks == {"above_tick": "0.05", + "below_tick": "0.01", + "cutoff_price": "3.00"} + From 802ab14da8bb4e32102fc49f4b8d2f63bc5eb378 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:43:53 -0700 Subject: [PATCH 38/65] and news/docs --- docs/stubs/pyrh.Robinhood.rst | 1 + newsfragments/201.feature | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/201.feature diff --git a/docs/stubs/pyrh.Robinhood.rst b/docs/stubs/pyrh.Robinhood.rst index 6917bfe8..88b5ac36 100644 --- a/docs/stubs/pyrh.Robinhood.rst +++ b/docs/stubs/pyrh.Robinhood.rst @@ -33,6 +33,7 @@ pyrh.Robinhood ~Robinhood.get_account ~Robinhood.get_fundamentals ~Robinhood.get_historical_quotes + ~Robinhood.get_instruments ~Robinhood.get_news ~Robinhood.get_open_orders ~Robinhood.get_option_chainid diff --git a/newsfragments/201.feature b/newsfragments/201.feature new file mode 100644 index 00000000..1dc6d864 --- /dev/null +++ b/newsfragments/201.feature @@ -0,0 +1 @@ +Add option model, refactor endpoints \ No newline at end of file From 01dedf019e2c3b21fc79ca60c6b8ae207f87df83 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:46:18 -0700 Subject: [PATCH 39/65] fix typos, requested changes --- pyrh/models/option.py | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index b1d4d454..30e95f6f 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,4 +1,4 @@ -"""Current portfolio.""" +"""Option data class""" from typing import Any @@ -16,31 +16,9 @@ class Option(BaseModel): class OptionSchema(BaseSchema): - """Robinhood Option schema data loader. - Sample result payload from - { - "chain_id": "cee01a93-626e-4ee6-9b04-60e2fd1392d1", - "chain_symbol": "AAPL", - "created_at": "2020-03-31T01:27:43.249339Z", - "expiration_date": "2020-04-17", - "id": "f098f169-74f9-4b91-b955-6834e1b67a12", - "issue_date": "2004-11-29", - "min_ticks": { - "above_tick": "0.05", - "below_tick": "0.01", - "cutoff_price": "3.00" - }, - "rhs_tradability": "untradable", - "state": "active", - "strike_price": "232.5000", - "tradability": "tradable", - "type": "put", - "updated_at": "2020-03-31T01:27:43.249354Z", - "url": "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" - } - """ + """Robinhood Option schema data loader. """ - __model__ = Options + __model__ = Option chain_id = fields.String() chain_symbol = fields.String() @@ -55,7 +33,7 @@ class OptionSchema(BaseSchema): tradability = fields.String() options_type = fields.String() updated_at = fields.NaiveDateTime() - url = fields.URL() + url = fields.URL() @post_load def make_object(self, data: JSON, **kwargs: Any) -> Option: @@ -71,10 +49,5 @@ def make_object(self, data: JSON, **kwargs: Any) -> Option: """ # Can potentially move this preprocessing part to a helper file prefix = self.__class__.__name__.lower() - reserved_word_overwrite = ["type", "id"] - for attr in reserved_word_overwrite: - k, v = f"{prefix}_{attr}", data.pop("attr") - data[k] = v - data = data.get("results", [{}])[0] return self.__model__(**data) From f1e4445ecc1c38a3d60c46bec4262b2f2edd77cf Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:48:29 -0700 Subject: [PATCH 40/65] fix indentation --- pyrh/robinhood.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index d49f31cf..dfb9e4df 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -54,7 +54,65 @@ def user(self): def investment_profile(self): """Fetch investment_profile.""" - return self.get(urls.INVESTMENT_PROFILE) + return self.get(endpoints.investment_profile()) + + def get_instruments(self, symbol, match=True, options=False): + """Query for instruments that match with the given ticker. + + Args: + symbol (str): stock ticker + match (bool): True if want exact match, False for partial match + + Returns: + (:obj: (list)): JSON contents from `instruments` endpoint - list + of instruments that match the ticker + """ + ticker = stock.upper() + params = {"symbol": ticker} if match else {"query": ticker} + res = self.get(endpoints.instruments(options=options), params=params) + results = res.get("results", []) + while res.get("next"): + res = res.get("next") + results.extend(res.get("results", [])) + return results + + def instruments(self, stock): + """Fetch instruments endpoint. + + Args: + stock (str): stock ticker + + Returns: + (:obj:`dict`): JSON contents from `instruments` endpoint + + """ + + res = self.get(endpoints.instruments(), params={"query": stock.upper()}) + + # if requesting all, return entire object so may paginate with ['next'] + if stock == "": + return res + + return res["results"] + + def instrument(self, id): + """Fetch instrument info. + + Args: + id (str): instrument id + + Returns: + (:obj:`dict`): JSON dict of instrument + + """ + url = str(endpoints.instruments()) + "?symbol=" + str(id) + + try: + data = requests.get(url) + except requests.exceptions.HTTPError: + raise InvalidInstrumentId() + + return data["results"][0] def quote_data(self, stock=""): """Fetch stock quote. From 81679311a49abdd728f3839df8fe2aa779d1a758 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:53:08 -0700 Subject: [PATCH 41/65] fix indentation --- pyrh/robinhood.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index dfb9e4df..9cff01ee 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -57,16 +57,16 @@ def investment_profile(self): return self.get(endpoints.investment_profile()) def get_instruments(self, symbol, match=True, options=False): - """Query for instruments that match with the given ticker. + """Query for instruments that match with the given ticker. - Args: - symbol (str): stock ticker - match (bool): True if want exact match, False for partial match + Args: + symbol (str): stock ticker + match (bool): True if want exact match, False for partial match - Returns: - (:obj: (list)): JSON contents from `instruments` endpoint - list - of instruments that match the ticker - """ + Returns: + (:obj: (list)): JSON contents from `instruments` endpoint - list + of instruments that match the ticker + """ ticker = stock.upper() params = {"symbol": ticker} if match else {"query": ticker} res = self.get(endpoints.instruments(options=options), params=params) From a78dd19ce1719e6c7d1817e4420d3b668206d03f Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 23:37:11 -0700 Subject: [PATCH 42/65] fix bugs, style --- newsfragments/201.feature | 2 +- pyrh/models/option.py | 11 +++++------ pyrh/robinhood.py | 6 ++---- tests/test_option.py | 13 ++++++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/newsfragments/201.feature b/newsfragments/201.feature index 1dc6d864..31a879b4 100644 --- a/newsfragments/201.feature +++ b/newsfragments/201.feature @@ -1 +1 @@ -Add option model, refactor endpoints \ No newline at end of file +Add option model, refactor endpoints diff --git a/pyrh/models/option.py b/pyrh/models/option.py index 30e95f6f..e67e4538 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,4 +1,4 @@ -"""Option data class""" +"""Option data class.""" from typing import Any @@ -10,13 +10,13 @@ class Option(BaseModel): - """Robinhood Option data class. Represents an options instrument""" + """Robinhood Option data class. Represents an options instrument.""" pass class OptionSchema(BaseSchema): - """Robinhood Option schema data loader. """ + """Robinhood Option schema data loader.""" __model__ = Option @@ -24,14 +24,14 @@ class OptionSchema(BaseSchema): chain_symbol = fields.String() created_at = fields.NaiveDateTime() expiration_date = fields.Date() - options_id = fields.String() + id = fields.String() issue_date = fields.Date() min_ticks = fields.Dict() rhs_tradability = fields.String() state = fields.String() strike_price = fields.Float() tradability = fields.String() - options_type = fields.String() + type = fields.String() updated_at = fields.NaiveDateTime() url = fields.URL() @@ -48,6 +48,5 @@ def make_object(self, data: JSON, **kwargs: Any) -> Option: """ # Can potentially move this preprocessing part to a helper file - prefix = self.__class__.__name__.lower() data = data.get("results", [{}])[0] return self.__model__(**data) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 9cff01ee..1f3d7cb3 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -67,7 +67,7 @@ def get_instruments(self, symbol, match=True, options=False): (:obj: (list)): JSON contents from `instruments` endpoint - list of instruments that match the ticker """ - ticker = stock.upper() + ticker = symbol.upper() params = {"symbol": ticker} if match else {"query": ticker} res = self.get(endpoints.instruments(options=options), params=params) results = res.get("results", []) @@ -512,9 +512,7 @@ def get_popularity(self, stock=""): """ stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] - return self.get_url(urls.build_instruments(stock_instrument, "popularity"))[ - "num_open_positions" - ] + return self.get(endpoints.popularity(stock_instrument))["num_open_positions"] def get_tickers_by_tag(self, tag=None): """Get a list of instruments belonging to a tag diff --git a/tests/test_option.py b/tests/test_option.py index 13fca96a..d9503d16 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -1,12 +1,14 @@ import datetime as dt import numbers -import requests from os import path + import pytest +import requests from six import string_types from pyrh.models.option import OptionSchema + HERE = path.abspath(path.dirname(__file__)) ROOT = path.dirname(HERE) @@ -44,7 +46,8 @@ def test_created_at(): def test_min_ticks(): min_ticks = option_obj.min_ticks assert isinstance(dict(), min_ticks) - assert min_ticks == {"above_tick": "0.05", - "below_tick": "0.01", - "cutoff_price": "3.00"} - + assert min_ticks == { + "above_tick": "0.05", + "below_tick": "0.01", + "cutoff_price": "3.00", + } From 845575c08efa9a5e86d4b426eb08746f534cd562 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 23:38:46 -0700 Subject: [PATCH 43/65] update known_third_party --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ddcd7a3a..751c2c99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ exclude = ''' [tool.isort] known_first_party = 'robinhood' -known_third_party = ["certifi", "dateutil", "freezegun", "marshmallow", "pytest", "pytz", "requests", "requests_mock", "yarl"] +known_third_party = ["certifi", "dateutil", "freezegun", "marshmallow", "pytest", "pytz", "requests", "requests_mock", "six", "yarl"] multi_line_output = 3 lines_after_imports = 2 force_grid_wrap = 0 From fca8680265ec88cc89a63979b8faba7d23292ce4 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 23:52:38 -0700 Subject: [PATCH 44/65] fix OptionSchema init --- tests/test_option.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_option.py b/tests/test_option.py index d9503d16..dbb42d94 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -16,7 +16,8 @@ sample_url = "https://api.robinhood.com/options/instruments/f098f169-74f9-4b91-b955-6834e1b67a12/" response = requests.get(sample_url) data = response.json() -option_obj = OptionSchema.make_object(data=data) +option_schema = OptionSchema() +option_obj = option_schema.make_object(data=data) def test_chain_symbol(): From 5f940945f8959a055304a59f669955238aac773b Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 15 Apr 2020 00:04:59 -0700 Subject: [PATCH 45/65] fix formatting of data in option --- pyrh/models/option.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index e67e4538..244db4a7 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -47,6 +47,4 @@ def make_object(self, data: JSON, **kwargs: Any) -> Option: An instance of the Option class. """ - # Can potentially move this preprocessing part to a helper file - data = data.get("results", [{}])[0] return self.__model__(**data) From ce4eb1b02c161fa44e2cb9eed3e83c5c5bc6bb1a Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 15 Apr 2020 00:22:44 -0700 Subject: [PATCH 46/65] fix order of isinstance args --- tests/test_option.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_option.py b/tests/test_option.py index dbb42d94..7bcba5dc 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -22,31 +22,31 @@ def test_chain_symbol(): symbol = option_obj.chain_symbol - assert isinstance(string_types, symbol) + assert isinstance(symbol, string_types) assert symbol == "AAPL" def test_strike_price(): strike = option_obj.strike - assert isinstance(numbers.Real, strike) + assert isinstance(strike, numbers.Real) assert format(strike, ".4f") == 232.5000 def test_expiration_date(): expiry = option_obj.expiration_date - assert isinstance(dt.datetime.date, expiry) + assert isinstance(expiry, dt.datetime.date) assert expiry == dt.date(2020, 4, 17) def test_created_at(): created_at = option_obj.created_at - assert isinstance(dt.datetime, created_at) + assert isinstance(created_at, dt.datetime) assert created_at == dt.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ") def test_min_ticks(): min_ticks = option_obj.min_ticks - assert isinstance(dict(), min_ticks) + assert isinstance(min_ticks, dict()) assert min_ticks == { "above_tick": "0.05", "below_tick": "0.01", From 036b83baf4c688566e2acc1083b01ad833cff02c Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 15 Apr 2020 00:31:26 -0700 Subject: [PATCH 47/65] fix attr name --- tests/test_option.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_option.py b/tests/test_option.py index 7bcba5dc..28cb0635 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -27,7 +27,7 @@ def test_chain_symbol(): def test_strike_price(): - strike = option_obj.strike + strike = option_obj.strike_price assert isinstance(strike, numbers.Real) assert format(strike, ".4f") == 232.5000 From 4e85da5c7d8582b01da392136561655e3fa3be3f Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 18 Apr 2020 00:00:32 -0700 Subject: [PATCH 48/65] fix option model, tests --- pyrh/models/option.py | 36 +++++++++++++++++++++++++++++++++--- tests/test_option.py | 22 ++++++++++------------ 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index 244db4a7..d846b56f 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -9,6 +9,36 @@ from .base import BaseModel, BaseSchema +class MinTicks(BaseModel): + """Min ticks data class. Describes min increments the option can be traded at.""" + + pass + + +class MinTicksSchema(BaseSchema): + """Min ticks schema data loader.""" + + __model__ = MinTicks + + above_tick = fields.Float() + below_tick = fields.Float() + cutoff_price = fields.Float() + + @post_load + def make_object(self, data: JSON, **kwargs: Any) -> MinTicks: + """Build model for the Min ticks class. + + Args: + data: The JSON diction to use to build the Min tick. + **kwargs: Unused but required to match signature of `Schema.make_object` + + Returns: + An instance of the Min tick class. + + """ + return self.__model__(**data) + + class Option(BaseModel): """Robinhood Option data class. Represents an options instrument.""" @@ -22,17 +52,17 @@ class OptionSchema(BaseSchema): chain_id = fields.String() chain_symbol = fields.String() - created_at = fields.NaiveDateTime() + created_at = fields.DateTime() expiration_date = fields.Date() id = fields.String() issue_date = fields.Date() - min_ticks = fields.Dict() + min_ticks = fields.Nested(MinTicksSchema) rhs_tradability = fields.String() state = fields.String() strike_price = fields.Float() tradability = fields.String() type = fields.String() - updated_at = fields.NaiveDateTime() + updated_at = fields.DateTime() url = fields.URL() @post_load diff --git a/tests/test_option.py b/tests/test_option.py index 28cb0635..f73730ca 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -2,11 +2,11 @@ import numbers from os import path -import pytest import requests +from dateutil import parser as p from six import string_types -from pyrh.models.option import OptionSchema +from pyrh.models.option import MinTicks, OptionSchema HERE = path.abspath(path.dirname(__file__)) @@ -17,7 +17,7 @@ response = requests.get(sample_url) data = response.json() option_schema = OptionSchema() -option_obj = option_schema.make_object(data=data) +option_obj = option_schema.load(data) def test_chain_symbol(): @@ -29,26 +29,24 @@ def test_chain_symbol(): def test_strike_price(): strike = option_obj.strike_price assert isinstance(strike, numbers.Real) - assert format(strike, ".4f") == 232.5000 + assert strike == 232.5 def test_expiration_date(): expiry = option_obj.expiration_date - assert isinstance(expiry, dt.datetime.date) + assert isinstance(expiry, dt.date) assert expiry == dt.date(2020, 4, 17) def test_created_at(): created_at = option_obj.created_at assert isinstance(created_at, dt.datetime) - assert created_at == dt.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ") + assert created_at == p.parse("2020-03-31T01:27:43.249339Z") def test_min_ticks(): min_ticks = option_obj.min_ticks - assert isinstance(min_ticks, dict()) - assert min_ticks == { - "above_tick": "0.05", - "below_tick": "0.01", - "cutoff_price": "3.00", - } + assert isinstance(min_ticks, MinTicks) + assert min_ticks.above_tick == 0.05 + assert min_ticks.below_tick == 0.01 + assert min_ticks.cutoff_price == 3.00 From 3d1e94341cf3c324e27f8681c7b15f408f79ae4d Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 18 Apr 2020 00:10:13 -0700 Subject: [PATCH 49/65] remove redundant code --- pyrh/models/option.py | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index d846b56f..f26ce6f2 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,10 +1,6 @@ """Option data class.""" -from typing import Any - -from marshmallow import fields, post_load - -from pyrh.common import JSON +from marshmallow import fields from .base import BaseModel, BaseSchema @@ -24,20 +20,6 @@ class MinTicksSchema(BaseSchema): below_tick = fields.Float() cutoff_price = fields.Float() - @post_load - def make_object(self, data: JSON, **kwargs: Any) -> MinTicks: - """Build model for the Min ticks class. - - Args: - data: The JSON diction to use to build the Min tick. - **kwargs: Unused but required to match signature of `Schema.make_object` - - Returns: - An instance of the Min tick class. - - """ - return self.__model__(**data) - class Option(BaseModel): """Robinhood Option data class. Represents an options instrument.""" @@ -64,17 +46,3 @@ class OptionSchema(BaseSchema): type = fields.String() updated_at = fields.DateTime() url = fields.URL() - - @post_load - def make_object(self, data: JSON, **kwargs: Any) -> Option: - """Build model for the Option class. - - Args: - data: The JSON diction to use to build the Option. - **kwargs: Unused but required to match signature of `Schema.make_object` - - Returns: - An instance of the Option class. - - """ - return self.__model__(**data) From 44b7ee8e729df0a9402dd342ac47d208576052a3 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 01:54:40 -0700 Subject: [PATCH 50/65] streamline instrument getter functions --- pyrh/robinhood.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 1f3d7cb3..57ef57c8 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -76,6 +76,27 @@ def get_instruments(self, symbol, match=True, options=False): results.extend(res.get("results", [])) return results + def get_instruments(self, symbol, match=True, options=False): + """Query for instruments that match with the given ticker. + + Args: + symbol (str): stock ticker + match (bool): True if want exact match, False for partial match + + Returns: + (:obj: (list)): JSON contents from `instruments` endpoint - list + of instruments that match the ticker + """ + ticker = stock.upper() + params = {"symbol": ticker} if match else {"query": ticker} + res = self.get(endpoints.instruments(options=options), params=params) + results = res.get("results", []) + while res.get("next"): + res = res.get("next") + results.extend(res.get("results", [])) + return results + + @deprecated def instruments(self, stock): """Fetch instruments endpoint. @@ -95,6 +116,7 @@ def instruments(self, stock): return res["results"] + @deprecated def instrument(self, id): """Fetch instrument info. From ea7550984eeddd63bc773bb17f51b88c03da78fd Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 20:29:46 -0700 Subject: [PATCH 51/65] remove deprecated --- pyrh/robinhood.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 57ef57c8..6b87f7ef 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -96,7 +96,6 @@ def get_instruments(self, symbol, match=True, options=False): results.extend(res.get("results", [])) return results - @deprecated def instruments(self, stock): """Fetch instruments endpoint. @@ -116,7 +115,6 @@ def instruments(self, stock): return res["results"] - @deprecated def instrument(self, id): """Fetch instrument info. From 734b2ac59243dc09324a4f7f093916dea01f9373 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:48:29 -0700 Subject: [PATCH 52/65] fix indentation --- pyrh/robinhood.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 6b87f7ef..5b304baf 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -87,14 +87,14 @@ def get_instruments(self, symbol, match=True, options=False): (:obj: (list)): JSON contents from `instruments` endpoint - list of instruments that match the ticker """ - ticker = stock.upper() - params = {"symbol": ticker} if match else {"query": ticker} - res = self.get(endpoints.instruments(options=options), params=params) - results = res.get("results", []) - while res.get("next"): - res = res.get("next") - results.extend(res.get("results", [])) - return results + ticker = stock.upper() + params = {"symbol": ticker} if match else {"query": ticker} + res = self.get(endpoints.instruments(options=options), params=params) + results = res.get("results", []) + while res.get("next"): + res = res.get("next") + results.extend(res.get("results", [])) + return results def instruments(self, stock): """Fetch instruments endpoint. From d42ba4f1c53697b9ac8d6fc64bd11474324d71fb Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 21:53:08 -0700 Subject: [PATCH 53/65] fix indentation --- pyrh/robinhood.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 5b304baf..41111403 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -77,16 +77,16 @@ def get_instruments(self, symbol, match=True, options=False): return results def get_instruments(self, symbol, match=True, options=False): - """Query for instruments that match with the given ticker. + """Query for instruments that match with the given ticker. - Args: - symbol (str): stock ticker - match (bool): True if want exact match, False for partial match + Args: + symbol (str): stock ticker + match (bool): True if want exact match, False for partial match - Returns: - (:obj: (list)): JSON contents from `instruments` endpoint - list - of instruments that match the ticker - """ + Returns: + (:obj: (list)): JSON contents from `instruments` endpoint - list + of instruments that match the ticker + """ ticker = stock.upper() params = {"symbol": ticker} if match else {"query": ticker} res = self.get(endpoints.instruments(options=options), params=params) From 93641bdfe45c14b8b9617c19d9b5d1436d91c97d Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 14 Apr 2020 23:37:11 -0700 Subject: [PATCH 54/65] fix bugs, style --- pyrh/robinhood.py | 2 +- tests/test_option.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 41111403..a21ae5d9 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -87,7 +87,7 @@ def get_instruments(self, symbol, match=True, options=False): (:obj: (list)): JSON contents from `instruments` endpoint - list of instruments that match the ticker """ - ticker = stock.upper() + ticker = symbol.upper() params = {"symbol": ticker} if match else {"query": ticker} res = self.get(endpoints.instruments(options=options), params=params) results = res.get("results", []) diff --git a/tests/test_option.py b/tests/test_option.py index f73730ca..0598fcf0 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -9,6 +9,7 @@ from pyrh.models.option import MinTicks, OptionSchema + HERE = path.abspath(path.dirname(__file__)) ROOT = path.dirname(HERE) From 50a019ed9436618e1ed41733c66a052e715e9cbb Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 18 Apr 2020 00:00:32 -0700 Subject: [PATCH 55/65] fix option model, tests --- pyrh/models/option.py | 30 ++++++++++++++++++++++++++++++ tests/test_option.py | 1 - 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index f26ce6f2..f03917e1 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -21,6 +21,36 @@ class MinTicksSchema(BaseSchema): cutoff_price = fields.Float() +class MinTicks(BaseModel): + """Min ticks data class. Describes min increments the option can be traded at.""" + + pass + + +class MinTicksSchema(BaseSchema): + """Min ticks schema data loader.""" + + __model__ = MinTicks + + above_tick = fields.Float() + below_tick = fields.Float() + cutoff_price = fields.Float() + + @post_load + def make_object(self, data: JSON, **kwargs: Any) -> MinTicks: + """Build model for the Min ticks class. + + Args: + data: The JSON diction to use to build the Min tick. + **kwargs: Unused but required to match signature of `Schema.make_object` + + Returns: + An instance of the Min tick class. + + """ + return self.__model__(**data) + + class Option(BaseModel): """Robinhood Option data class. Represents an options instrument.""" diff --git a/tests/test_option.py b/tests/test_option.py index 0598fcf0..f73730ca 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -9,7 +9,6 @@ from pyrh.models.option import MinTicks, OptionSchema - HERE = path.abspath(path.dirname(__file__)) ROOT = path.dirname(HERE) From ea407fe26b4eb228f5366b9d3c89cb73095817c5 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 18 Apr 2020 00:10:13 -0700 Subject: [PATCH 56/65] remove redundant code --- pyrh/models/option.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index f03917e1..a2a60f5c 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -36,20 +36,6 @@ class MinTicksSchema(BaseSchema): below_tick = fields.Float() cutoff_price = fields.Float() - @post_load - def make_object(self, data: JSON, **kwargs: Any) -> MinTicks: - """Build model for the Min ticks class. - - Args: - data: The JSON diction to use to build the Min tick. - **kwargs: Unused but required to match signature of `Schema.make_object` - - Returns: - An instance of the Min tick class. - - """ - return self.__model__(**data) - class Option(BaseModel): """Robinhood Option data class. Represents an options instrument.""" From 91a37b05b92c53de544f46629dea438efe996720 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 18 Apr 2020 13:45:24 -0700 Subject: [PATCH 57/65] update endpoints to urls --- pyrh/robinhood.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index a21ae5d9..7360b48d 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -69,7 +69,7 @@ def get_instruments(self, symbol, match=True, options=False): """ ticker = symbol.upper() params = {"symbol": ticker} if match else {"query": ticker} - res = self.get(endpoints.instruments(options=options), params=params) + res = self.get(urls.instruments(options=options), params=params) results = res.get("results", []) while res.get("next"): res = res.get("next") @@ -521,19 +521,6 @@ def get_url(self, url): return self.get(url) - def get_popularity(self, stock=""): - """Get the number of robinhood users who own the given stock - - Args: - stock (str): stock ticker - - Returns: - (int): number of users who own the stock - - """ - stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] - return self.get(endpoints.popularity(stock_instrument))["num_open_positions"] - def get_tickers_by_tag(self, tag=None): """Get a list of instruments belonging to a tag From e6e88ea4de0ede1a318f3c80b573682311725b22 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 19 Apr 2020 19:33:11 -0700 Subject: [PATCH 58/65] fix merge conflicts --- pyrh/models/option.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index a2a60f5c..f26ce6f2 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -5,22 +5,6 @@ from .base import BaseModel, BaseSchema -class MinTicks(BaseModel): - """Min ticks data class. Describes min increments the option can be traded at.""" - - pass - - -class MinTicksSchema(BaseSchema): - """Min ticks schema data loader.""" - - __model__ = MinTicks - - above_tick = fields.Float() - below_tick = fields.Float() - cutoff_price = fields.Float() - - class MinTicks(BaseModel): """Min ticks data class. Describes min increments the option can be traded at.""" From 1cc267fc4aace95fe8d0e04e8ee21814a574c8fa Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 19 Apr 2020 20:49:27 -0700 Subject: [PATCH 59/65] fix merge conflicts --- pyrh/robinhood.py | 60 +---------------------------------------------- 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 7360b48d..9d53200d 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -54,7 +54,7 @@ def user(self): def investment_profile(self): """Fetch investment_profile.""" - return self.get(endpoints.investment_profile()) + return self.get(urls.investment_profile()) def get_instruments(self, symbol, match=True, options=False): """Query for instruments that match with the given ticker. @@ -76,64 +76,6 @@ def get_instruments(self, symbol, match=True, options=False): results.extend(res.get("results", [])) return results - def get_instruments(self, symbol, match=True, options=False): - """Query for instruments that match with the given ticker. - - Args: - symbol (str): stock ticker - match (bool): True if want exact match, False for partial match - - Returns: - (:obj: (list)): JSON contents from `instruments` endpoint - list - of instruments that match the ticker - """ - ticker = symbol.upper() - params = {"symbol": ticker} if match else {"query": ticker} - res = self.get(endpoints.instruments(options=options), params=params) - results = res.get("results", []) - while res.get("next"): - res = res.get("next") - results.extend(res.get("results", [])) - return results - - def instruments(self, stock): - """Fetch instruments endpoint. - - Args: - stock (str): stock ticker - - Returns: - (:obj:`dict`): JSON contents from `instruments` endpoint - - """ - - res = self.get(endpoints.instruments(), params={"query": stock.upper()}) - - # if requesting all, return entire object so may paginate with ['next'] - if stock == "": - return res - - return res["results"] - - def instrument(self, id): - """Fetch instrument info. - - Args: - id (str): instrument id - - Returns: - (:obj:`dict`): JSON dict of instrument - - """ - url = str(endpoints.instruments()) + "?symbol=" + str(id) - - try: - data = requests.get(url) - except requests.exceptions.HTTPError: - raise InvalidInstrumentId() - - return data["results"][0] - def quote_data(self, stock=""): """Fetch stock quote. From 40dcae90a0d395771a1def363ebca05d611d4784 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 19 Apr 2020 20:54:40 -0700 Subject: [PATCH 60/65] fix merge conflicts --- pyrh/robinhood.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 9d53200d..1e6e5d99 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -54,27 +54,19 @@ def user(self): def investment_profile(self): """Fetch investment_profile.""" - return self.get(urls.investment_profile()) - - def get_instruments(self, symbol, match=True, options=False): - """Query for instruments that match with the given ticker. + return self.get(urls.INVESTMENT_PROFILE) + def get_popularity(self, stock=""): + """Get the number of robinhood users who own the given stock Args: - symbol (str): stock ticker - match (bool): True if want exact match, False for partial match - + stock (str): stock ticker Returns: - (:obj: (list)): JSON contents from `instruments` endpoint - list - of instruments that match the ticker + (int): number of users who own the stock """ - ticker = symbol.upper() - params = {"symbol": ticker} if match else {"query": ticker} - res = self.get(urls.instruments(options=options), params=params) - results = res.get("results", []) - while res.get("next"): - res = res.get("next") - results.extend(res.get("results", [])) - return results + stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] + return self.get_url(urls.build_instruments(stock_instrument, "popularity"))[ + "num_open_positions" + ] def quote_data(self, stock=""): """Fetch stock quote. From 3f0185a232ffb1bc0f18c6d845396d1640293275 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 20 Apr 2020 22:14:04 -0700 Subject: [PATCH 61/65] add chain model, basic tests --- pyrh/models/chain.py | 58 +++++++++++++++++++++++++++++++++++++++++++ pyrh/models/option.py | 24 +++++++++++++++--- pyrh/robinhood.py | 24 +++++++++--------- tests/test_chain.py | 36 +++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 pyrh/models/chain.py create mode 100644 tests/test_chain.py diff --git a/pyrh/models/chain.py b/pyrh/models/chain.py new file mode 100644 index 00000000..30d79e4d --- /dev/null +++ b/pyrh/models/chain.py @@ -0,0 +1,58 @@ +"""Option Chain data class.""" + +from typing import Any, Iterable + +from marshmallow import fields + +from pyrh import urls + +from .base import BaseModel, BaseSchema, base_paginator +from .option import MinTicksSchema, Option, OptionPaginatorSchema + + +class Chain(BaseModel): + """Chain data class. Represents an option chain.""" + + def get_chain(self, **kwargs: Any) -> Iterable[Option]: + """Get a generator of options consisting of the option chain. + + Args: + **kwargs: If the query argument is provided, the returned values will + be restricted to option instruments that match the query. Possible + query parameters: state (active), tradability, type (call vs put), + expiration_dates, strike_price, chain_id + + Returns: + A generator of Options. + """ + query = {"chain_id": str(self.id), "chain_symbol": self.symbol} + valid_params = frozenset( + [ + "chain_symbol", + "state", + "tradability", + "type", + "expiration_dates", + "strike_price", + "chain_id", + ] + ) + query.update({k: v for k, v in kwargs.items() if k in valid_params}) + # TODO should we allow chain_symbol, chain_id to be overwritten? + url = urls.OPTIONS_INSTRUMENTS_BASE.with_query(**query) + return base_paginator(url, self, OptionPaginatorSchema()) + + +class ChainSchema(BaseSchema): + """Chain schema data loader.""" + + __model__ = Chain + + id = fields.UUID() + symbol = fields.Str() + can_open_position = fields.Bool() + cash_component = fields.Float(allow_none=True) + expiration_dates = fields.List(fields.Str()) + trade_value_multiplier = fields.Float() + underlying_instruments = fields.List(fields.Dict()) + min_ticks = fields.Nested(MinTicksSchema) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index f26ce6f2..fe36bc4f 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -2,7 +2,7 @@ from marshmallow import fields -from .base import BaseModel, BaseSchema +from .base import BaseModel, BasePaginator, BasePaginatorSchema, BaseSchema class MinTicks(BaseModel): @@ -32,11 +32,11 @@ class OptionSchema(BaseSchema): __model__ = Option - chain_id = fields.String() + chain_id = fields.UUID() chain_symbol = fields.String() created_at = fields.DateTime() expiration_date = fields.Date() - id = fields.String() + id = fields.UUID() issue_date = fields.Date() min_ticks = fields.Nested(MinTicksSchema) rhs_tradability = fields.String() @@ -46,3 +46,21 @@ class OptionSchema(BaseSchema): type = fields.String() updated_at = fields.DateTime() url = fields.URL() + + +class OptionPaginator(BasePaginator): + """Thin wrapper around `self.results`, a list of `Option` objs.""" + + pass + + +class OptionPaginatorSchema(BasePaginatorSchema): + """Schema class for the OptionPaginator. + + The nested results are of type `Option`. + + """ + + __model__ = OptionPaginator + + results = fields.List(fields.Nested(OptionSchema)) diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 1e6e5d99..204ded00 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -56,18 +56,6 @@ def investment_profile(self): """Fetch investment_profile.""" return self.get(urls.INVESTMENT_PROFILE) - def get_popularity(self, stock=""): - """Get the number of robinhood users who own the given stock - Args: - stock (str): stock ticker - Returns: - (int): number of users who own the stock - """ - stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] - return self.get_url(urls.build_instruments(stock_instrument, "popularity"))[ - "num_open_positions" - ] - def quote_data(self, stock=""): """Fetch stock quote. @@ -95,6 +83,18 @@ def quote_data(self, stock=""): return data + def get_popularity(self, stock=""): + """Get the number of robinhood users who own the given stock + Args: + stock (str): stock ticker + Returns: + (int): number of users who own the stock + """ + stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] + return self.get_url(urls.build_instruments(stock_instrument, "popularity"))[ + "num_open_positions" + ] + # We will keep for compatibility until next major release def quotes_data(self, stocks): """Fetch quote for multiple stocks, in one single Robinhood API call. diff --git a/tests/test_chain.py b/tests/test_chain.py new file mode 100644 index 00000000..ea5aa51a --- /dev/null +++ b/tests/test_chain.py @@ -0,0 +1,36 @@ +import datetime as dt +import numbers +from os import path + +import requests +from six import string_types + +from pyrh.models.chain import Chain, ChainSchema +from pyrh.models.option import MinTicks + + +HERE = path.abspath(path.dirname(__file__)) +ROOT = path.dirname(HERE) + +# Setup +sample_url = ( + "https://api.robinhood.com/options/chains/cee01a93-626e-4ee6-9b04-60e2fd1392d1/" +) +response = requests.get(sample_url) +data = response.json() +chain_schema = ChainSchema() +chain_obj = chain_schema.load(data) + + +def test_chain_symbol(): + symbol = chain_obj.symbol + assert isinstance(symbol, string_types) + assert symbol == "AAPL" + + +def test_min_ticks(): + min_ticks = chain_obj.min_ticks + assert isinstance(min_ticks, MinTicks) + assert min_ticks.above_tick == 0.05 + assert min_ticks.below_tick == 0.01 + assert min_ticks.cutoff_price == 3.00 From 9287b4fceafb8bb8ac8383962d6c903d1ec0524d Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 25 Apr 2020 17:16:02 -0700 Subject: [PATCH 62/65] Add option manager, paginator --- pyrh/models/__init__.py | 12 ++++++++++++ pyrh/models/chain.py | 37 +++-------------------------------- pyrh/models/option.py | 43 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 35 deletions(-) diff --git a/pyrh/models/__init__.py b/pyrh/models/__init__.py index 1d3a90ba..b6cb6d3b 100644 --- a/pyrh/models/__init__.py +++ b/pyrh/models/__init__.py @@ -8,6 +8,13 @@ InstrumentSchema, ) from .oauth import Challenge, ChallengeSchema, OAuth, OAuthSchema +from .option import ( + Option, + OptionManager, + OptionPaginator, + OptionPaginatorSchema, + OptionSchema, +) from .portfolio import Portfolio, PortfolioSchema from .sessionmanager import SessionManager, SessionManagerSchema @@ -26,4 +33,9 @@ "InstrumentManager", "InstrumentPaginator", "InstrumentPaginatorSchema", + "Option", + "OptionSchema", + "OptionManager", + "OptionPaginator", + "OptionPaginatorSchema", ] diff --git a/pyrh/models/chain.py b/pyrh/models/chain.py index 30d79e4d..c4e70a8f 100644 --- a/pyrh/models/chain.py +++ b/pyrh/models/chain.py @@ -1,46 +1,15 @@ """Option Chain data class.""" -from typing import Any, Iterable - from marshmallow import fields -from pyrh import urls - -from .base import BaseModel, BaseSchema, base_paginator -from .option import MinTicksSchema, Option, OptionPaginatorSchema +from .base import BaseModel, BaseSchema +from .option import MinTicksSchema class Chain(BaseModel): """Chain data class. Represents an option chain.""" - def get_chain(self, **kwargs: Any) -> Iterable[Option]: - """Get a generator of options consisting of the option chain. - - Args: - **kwargs: If the query argument is provided, the returned values will - be restricted to option instruments that match the query. Possible - query parameters: state (active), tradability, type (call vs put), - expiration_dates, strike_price, chain_id - - Returns: - A generator of Options. - """ - query = {"chain_id": str(self.id), "chain_symbol": self.symbol} - valid_params = frozenset( - [ - "chain_symbol", - "state", - "tradability", - "type", - "expiration_dates", - "strike_price", - "chain_id", - ] - ) - query.update({k: v for k, v in kwargs.items() if k in valid_params}) - # TODO should we allow chain_symbol, chain_id to be overwritten? - url = urls.OPTIONS_INSTRUMENTS_BASE.with_query(**query) - return base_paginator(url, self, OptionPaginatorSchema()) + pass class ChainSchema(BaseSchema): diff --git a/pyrh/models/option.py b/pyrh/models/option.py index fe36bc4f..010839bd 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,8 +1,18 @@ """Option data class.""" +from typing import Any, Iterable from marshmallow import fields -from .base import BaseModel, BasePaginator, BasePaginatorSchema, BaseSchema +from pyrh import urls + +from .base import ( + BaseModel, + BasePaginator, + BasePaginatorSchema, + BaseSchema, + base_paginator, +) +from .sessionmanager import SessionManager class MinTicks(BaseModel): @@ -64,3 +74,34 @@ class OptionPaginatorSchema(BasePaginatorSchema): __model__ = OptionPaginator results = fields.List(fields.Nested(OptionSchema)) + + +class OptionManager(SessionManager): + """Group together methods that manipulate an options.""" + + def get_options(self, **kwargs: Any) -> Iterable[Option]: + """Get a generator of options. + + Args: + **kwargs: If the query argument is provided, the returned values will + be restricted to option instruments that match the query. Possible + query parameters: chain_symbol, chain_id, state (active), + tradability, type (call vs put), expiration_dates, strike_price + + Returns: + A generator of Options. + """ + valid_params = frozenset( + [ + "chain_symbol", + "state", + "tradability", + "type", + "expiration_dates", + "strike_price", + "chain_id", + ] + ) + query = {k: v for k, v in kwargs.items() if k in valid_params} + url = urls.OPTIONS_INSTRUMENTS_BASE.with_query(**query) + return base_paginator(url, self, OptionPaginatorSchema()) From 3c0d61d0091d56803dd327699247d1fb60103313 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 2 May 2020 18:38:45 -0700 Subject: [PATCH 63/65] add repr for option model --- pyrh/models/option.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index 010839bd..c8492627 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -34,7 +34,17 @@ class MinTicksSchema(BaseSchema): class Option(BaseModel): """Robinhood Option data class. Represents an options instrument.""" - pass + def __repr__(self) -> str: + """Return the object as a string. + + Returns: + The string representation of the object. + + """ + return ( + f"Option<{self.chain_symbol}|{self.strike_price}|" + + f"{self.type}|{self.expiration_date}>" + ) class OptionSchema(BaseSchema): From 1372cb46e498b828889b5cfcb739e07806091be4 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 3 May 2020 17:06:04 -0700 Subject: [PATCH 64/65] merge from master, get_option_positions using model --- pyrh/models/option.py | 85 ++++++++++++++++++++++++++++++- pyrh/robinhood.py | 116 +++++++++++++++++++++++------------------- 2 files changed, 148 insertions(+), 53 deletions(-) diff --git a/pyrh/models/option.py b/pyrh/models/option.py index c8492627..b69100e9 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -1,7 +1,8 @@ """Option data class.""" -from typing import Any, Iterable +from typing import Any, Iterable, cast from marshmallow import fields +from yarl import URL from pyrh import urls @@ -86,9 +87,91 @@ class OptionPaginatorSchema(BasePaginatorSchema): results = fields.List(fields.Nested(OptionSchema)) +class OptionPosition(BaseModel): + """Robinhood Option position data class. Represents an option position.""" + + def __repr__(self) -> str: + """Return the object as a string. + + Returns: + The string representation of the object. + + """ + underlying = getattr(self, "underlying", self.option) + return f"OptionPosition<{underlying}|{self.quantity}|{self.type}>" + + +class OptionPositionSchema(BaseSchema): + """Robinhood Option position schema data loader.""" + + __model__ = OptionPosition + + account = (fields.URL(),) + average_price = (fields.Float(),) + chain_id = (fields.UUID(),) + chain_symbol = (fields.String(),) + id = (fields.UUID(),) + option = (fields.URL(),) + type = (fields.String(),) # should this be an enum? + pending_buy_quantity = (fields.Float(),) + pending_expired_quantity = (fields.Float(),) + pending_expiration_quantity = (fields.Float(),) + pending_exercise_quantity = (fields.Float(),) + pending_assignment_quantity = (fields.Float(),) + pending_sell_quantity = (fields.Float(),) + quantity = (fields.Float(),) + intraday_quantity = (fields.Float(),) + intraday_average_open_price = (fields.Float(),) + created_at = (fields.DateTime(),) + trade_value_multiplier = (fields.Float(),) + updated_at = (fields.DateTime(),) + url = (fields.URL(),) + + +class OptionPositionPaginator(BasePaginator): + """Thin wrapper around `self.results`, a list of `Option` objs.""" + + pass + + +class OptionPositionPaginatorSchema(BasePaginatorSchema): + """Schema class for the OptionPaginator. + + The nested results are of type `OptionPosition`. + + """ + + __model__ = OptionPositionPaginator + + results = fields.List(fields.Nested(OptionPositionSchema)) + + class OptionManager(SessionManager): """Group together methods that manipulate an options.""" + def _get_option_from_url(self, option_url: URL) -> Option: + """Get option from option_url. + + Args: + option_url: url to the option, used for getting the underlying option + for an options position. + + Returns: + An Option object. + """ + option_data = self.get(option_url) + return cast(Option, OptionSchema().load(option_data)) + + def _get_option_positions(self, open_pos: bool = True) -> Iterable[OptionPosition]: + # TODO figure out what /?nonzero=true is, returns quantity = 0... + url = urls.OPTIONS_BASE.join(URL("positions/?nonzero=true")) + positions = base_paginator(url, self, OptionPositionPaginatorSchema()) + if open_pos: + positions = [p for p in positions if float(p.quantity) > 0] + for p in positions: + p.underlying = self._get_option_from_url(p.option) + return positions + def get_options(self, **kwargs: Any) -> Iterable[Option]: """Get a generator of options. diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 204ded00..0dfb381e 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -1,15 +1,19 @@ """robinhood.py: a collection of utilities for working with Robinhood's Private API.""" from enum import Enum +from typing import Iterable from urllib.parse import unquote import dateutil import requests +from yarl import URL from pyrh import urls from pyrh.exceptions import InvalidTickerSymbol from pyrh.models import ( InstrumentManager, + Option, + OptionManager, PortfolioSchema, SessionManager, SessionManagerSchema, @@ -33,7 +37,7 @@ class Transaction(Enum): SELL = "sell" -class Robinhood(InstrumentManager, SessionManager): +class Robinhood(InstrumentManager, OptionManager, SessionManager): """Wrapper class for fetching/parsing Robinhood endpoints. Please see :py:class:`SessionManager` for login functionality. @@ -83,18 +87,6 @@ def quote_data(self, stock=""): return data - def get_popularity(self, stock=""): - """Get the number of robinhood users who own the given stock - Args: - stock (str): stock ticker - Returns: - (int): number of users who own the stock - """ - stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] - return self.get_url(urls.build_instruments(stock_instrument, "popularity"))[ - "num_open_positions" - ] - # We will keep for compatibility until next major release def quotes_data(self, stocks): """Fetch quote for multiple stocks, in one single Robinhood API call. @@ -197,16 +189,13 @@ def get_historical_quotes(self, stock, interval, span, bounds=Bounds.REGULAR): if isinstance(bounds, str): # recast to Enum bounds = Bounds(bounds) - historicals = ( - urls.HISTORICALS - + "/?symbols=" - + ",".join(stock).upper() - + "&interval=" - + interval - + "&span=" - + span - + "&bounds=" - + bounds.name.lower() + historicals = urls.HISTORICALS.with_query( + [ + ("symbols", ",".join(stock).upper()), + ("interval", interval), + ("span", span), + ("bounds", bounds.name.lower()), + ] ) return self.get(historicals) @@ -455,6 +444,21 @@ def get_url(self, url): return self.get(url) + def get_popularity(self, stock=""): + """Get the number of robinhood users who own the given stock + + Args: + stock (str): stock ticker + + Returns: + (int): number of users who own the stock + + """ + stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] + return self.get_url(urls.build_instruments(stock_instrument, "popularity"))[ + "num_open_positions" + ] + def get_tickers_by_tag(self, tag=None): """Get a list of instruments belonging to a tag @@ -519,23 +523,30 @@ def get_options(self, stock, expiration_dates, option_type): # raise InvalidOptionId() # return market_data + def get_option_positions(self, open_pos: bool = True) -> Iterable[Option]: + # TODO figure out what /?nonzero=true is, returns quantity = 0... + return self._get_option_positions(open_pos) + # url = urls.OPTIONS_BASE.join(URL("positions/?nonzero=true")) + # options = base_paginator(url, self, OptionPaginatorSchema()) + # if open_pos: + # filter_ = lambda x: float(x.get("quantity")) > 0 + # options = [o for o in options if filter_(o)] + # return options + def options_owned(self): - options = self.get_url(urls.options_base() + "positions/?nonzero=true") + options = self.get_url(urls.OPTIONS_BASE.join(URL("positions/?nonzero=true"))) options = options["results"] return options - def get_option_marketdata(self, instrument): - info = self.get_url( - urls.build_market_data() + "options/?instruments=" + instrument - ) - return info["results"][0] + def get_option_marketdata(self, option_id): + info = self.get_url(urls.MARKET_DATA_BASE.join(URL(f"options/{option_id}/"))) + return info def get_option_chainid(self, symbol): - stock_info = self.get_url(self.endpoints["instruments"] + "?symbol=" + symbol) - stock_id = stock_info["results"][0]["id"] - params = {} - params["equity_instrument_ids"] = stock_id - chains = self.get_url(urls.options_base() + "chains/", params=params) + stock_info = self.get_url(urls.INSTRUMENTS_BASE.with_query(symbol=symbol)) + instrument_id = stock_info["results"][0]["id"] + url = urls.OPTIONS_BASE.join(URL("chains/")) + chains = self.get_url(url.with_query(equity_instrument_ids=instrument_id)) chains = chains["results"] chain_id = None @@ -545,24 +556,25 @@ def get_option_chainid(self, symbol): return chain_id - def get_option_quote(self, arg_dict): - chain_id = self.get_option_chainid(arg_dict.pop("symbol", None)) - arg_dict["chain_id"] = chain_id - option_info = self.get_url( - self.endpoints.options_base() + "instruments/", params=arg_dict - ) - option_info = option_info["results"] - exp_price_list = [] - - for op in option_info: - mrkt_data = self.get_option_marketdata(op["url"]) - op_price = mrkt_data["adjusted_mark_price"] - exp = op["expiration_date"] - exp_price_list.append((exp, op_price)) - - exp_price_list.sort() - - return exp_price_list + def get_option_quote(self, symbol, strike, expiry, otype, state="active"): + url = urls.OPTIONS_BASE.join(URL("instruments/")) + params = { + "chain_symbol": symbol, + "strike_price": strike, + "expiration_dates": expiry, + "type": otype, + "state": state, + } + # symbol, strike, expiry, otype should uniquely define an option + results = self.get_url(url.with_query(**params)).get("results") + if not results: + return + else: + option_id = results[0]["id"] + result = self.get_option_marketdata(option_id) + params["ask"] = "{} x {}".format(result["ask_size"], result["ask_price"]) + params["bid"] = "{} x {}".format(result["bid_size"], result["bid_price"]) + return params ########################################################################### # GET FUNDAMENTALS From efcc31bda622ede942cef10292e22e29258f5256 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 3 May 2020 18:51:58 -0700 Subject: [PATCH 65/65] get option quote using model, update news --- newsfragments/224.feature | 1 + pyrh/models/__init__.py | 8 ++++ pyrh/models/option.py | 93 ++++++++++++++++++++++++++++++++++++++- pyrh/robinhood.py | 29 ++++-------- pyrh/urls.py | 2 +- 5 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 newsfragments/224.feature diff --git a/newsfragments/224.feature b/newsfragments/224.feature new file mode 100644 index 00000000..62a8cd68 --- /dev/null +++ b/newsfragments/224.feature @@ -0,0 +1 @@ +Add options related models and add new options related functions to robinhood.py. diff --git a/pyrh/models/__init__.py b/pyrh/models/__init__.py index b6cb6d3b..4182fdd6 100644 --- a/pyrh/models/__init__.py +++ b/pyrh/models/__init__.py @@ -13,6 +13,10 @@ OptionManager, OptionPaginator, OptionPaginatorSchema, + OptionPosition, + OptionPositionSchema, + OptionQuote, + OptionQuoteSchema, OptionSchema, ) from .portfolio import Portfolio, PortfolioSchema @@ -35,6 +39,10 @@ "InstrumentPaginatorSchema", "Option", "OptionSchema", + "OptionPosition", + "OptionPositionSchema", + "OptionQuote", + "OptionQuoteSchema", "OptionManager", "OptionPaginator", "OptionPaginatorSchema", diff --git a/pyrh/models/option.py b/pyrh/models/option.py index b69100e9..a37088c4 100644 --- a/pyrh/models/option.py +++ b/pyrh/models/option.py @@ -4,7 +4,7 @@ from marshmallow import fields from yarl import URL -from pyrh import urls +from pyrh import exceptions, urls from .base import ( BaseModel, @@ -128,6 +128,62 @@ class OptionPositionSchema(BaseSchema): url = (fields.URL(),) +class OptionQuote(BaseModel): + """Robinhood Option quote data class. Represents an option quote.""" + + def __repr__(self) -> str: + """Return the object as a string. + + Returns: + The string representation of the object. + + """ + return f"""OptionQuote< + Ask: {self.ask_size} x {self.ask_price} + Bid: {self.bid_size} x {self.bid_price} + Low: {self.low_price} | High: {self.high_price} + Volume: {self.volume} | Open Interest: {self.open_interest} + Implied Volatility: {self.implied_volatility} + Delta: {self.delta} | Gamma: {self.gamma} | Rho: {self.rho} + Theta: {self.theta} | Vega: {self.vega} + >""" + + +class OptionQuoteSchema(BaseSchema): + """Robinhood Option quote schema data loader.""" + + __model__ = OptionQuote + + adjusted_mark_price = (fields.Float(),) + ask_price = (fields.Float(),) + ask_size = (fields.Integer(),) + bid_price = (fields.Float(),) + bid_size = (fields.Integer(),) + break_even_price = (fields.Float(),) + high_price = (fields.Float(),) + instrument = (fields.URL(),) + last_trade_price = (fields.Float(),) + last_trade_size = (fields.Integer(),) + low_price = (fields.Float(),) + mark_price = (fields.Float(),) + open_interest = (fields.Integer(),) + previous_close_date = (fields.Date(),) + previous_close_price = (fields.Float(),) + volume = (fields.Integer(),) + chance_of_profit_long = (fields.Float(),) + chance_of_profit_short = (fields.Float(),) + delta = (fields.Float(),) + gamma = (fields.Float(),) + implied_volatility = (fields.Float(),) + rho = (fields.Float(),) + theta = (fields.Float(),) + vega = (fields.Float(),) + high_fill_rate_buy_price = (fields.Float(),) + high_fill_rate_sell_price = (fields.Float(),) + low_fill_rate_buy_price = (fields.Float(),) + low_fill_rate_sell_price = (fields.Float(),) + + class OptionPositionPaginator(BasePaginator): """Thin wrapper around `self.results`, a list of `Option` objs.""" @@ -162,6 +218,20 @@ def _get_option_from_url(self, option_url: URL) -> Option: option_data = self.get(option_url) return cast(Option, OptionSchema().load(option_data)) + def _get_option_quote(self, option_id: str) -> OptionQuote: + """Get quote from option id. + + Args: + option_id: underlying option id to get quote for + + Returns: + An OptionQuote object. + """ + quote_data = self.get_url( + urls.MARKET_DATA_BASE.join(URL(f"options/{option_id}/")) + ) + return cast(OptionQuote, OptionQuoteSchema().load(quote_data)) + def _get_option_positions(self, open_pos: bool = True) -> Iterable[OptionPosition]: # TODO figure out what /?nonzero=true is, returns quantity = 0... url = urls.OPTIONS_BASE.join(URL("positions/?nonzero=true")) @@ -172,6 +242,27 @@ def _get_option_positions(self, open_pos: bool = True) -> Iterable[OptionPositio p.underlying = self._get_option_from_url(p.option) return positions + def _get_option_id( + self, symbol: str, strike: str, expiry: str, otype: str, state: str = "active" + ) -> str: + url = urls.OPTIONS_BASE.join(URL("instruments/")) + params = { + "chain_symbol": symbol, + "strike_price": strike, + "expiration_dates": expiry, + "type": otype, + "state": state, + } + results = self.get_url(url.with_query(**params)).get("results") + if not results: + e = """ + Couldn't find option with symbol={}, strike={}, expiry={}, type={}, state={} + """.format( + symbol, strike, expiry, otype, state + ) + raise exceptions.InvalidOptionId(e) + return str(results[0]["id"]) + def get_options(self, **kwargs: Any) -> Iterable[Option]: """Get a generator of options. diff --git a/pyrh/robinhood.py b/pyrh/robinhood.py index 0dfb381e..67efee63 100644 --- a/pyrh/robinhood.py +++ b/pyrh/robinhood.py @@ -12,8 +12,9 @@ from pyrh.exceptions import InvalidTickerSymbol from pyrh.models import ( InstrumentManager, - Option, OptionManager, + OptionPosition, + OptionQuote, PortfolioSchema, SessionManager, SessionManagerSchema, @@ -523,7 +524,7 @@ def get_options(self, stock, expiration_dates, option_type): # raise InvalidOptionId() # return market_data - def get_option_positions(self, open_pos: bool = True) -> Iterable[Option]: + def get_option_positions(self, open_pos: bool = True) -> Iterable[OptionPosition]: # TODO figure out what /?nonzero=true is, returns quantity = 0... return self._get_option_positions(open_pos) # url = urls.OPTIONS_BASE.join(URL("positions/?nonzero=true")) @@ -556,25 +557,11 @@ def get_option_chainid(self, symbol): return chain_id - def get_option_quote(self, symbol, strike, expiry, otype, state="active"): - url = urls.OPTIONS_BASE.join(URL("instruments/")) - params = { - "chain_symbol": symbol, - "strike_price": strike, - "expiration_dates": expiry, - "type": otype, - "state": state, - } - # symbol, strike, expiry, otype should uniquely define an option - results = self.get_url(url.with_query(**params)).get("results") - if not results: - return - else: - option_id = results[0]["id"] - result = self.get_option_marketdata(option_id) - params["ask"] = "{} x {}".format(result["ask_size"], result["ask_price"]) - params["bid"] = "{} x {}".format(result["bid_size"], result["bid_price"]) - return params + def get_option_quote( + self, symbol, strike, expiry, otype, state="active" + ) -> OptionQuote: + option_id = self._get_option_id(symbol, strike, expiry, otype, state) + return self._get_option_quote(option_id) ########################################################################### # GET FUNDAMENTALS diff --git a/pyrh/urls.py b/pyrh/urls.py index a611d4ac..e6dc2877 100755 --- a/pyrh/urls.py +++ b/pyrh/urls.py @@ -21,7 +21,7 @@ INSTRUMENTS_BASE = API_BASE / "instruments/" MARGIN_UPGRADES = API_BASE / "margin/upgrades/" # not implemented MARKETS = API_BASE / "markets/" # not implemented -MARKET_DATA_BASE = API_BASE / "marketdata/options/" +MARKET_DATA_BASE = API_BASE / "marketdata/" NEWS_BASE = API_BASE / "midlands/news/" NOTIFICATIONS = API_BASE / "notifications/" # not implemented ORDERS_BASE = API_BASE / "orders/"