From 6459dc618b9311fbde8f34eda663fcbf84ca9e55 Mon Sep 17 00:00:00 2001 From: Graeme22 Date: Wed, 5 Jul 2023 18:46:58 -0500 Subject: [PATCH 1/3] versioning bumps, docs tweaks --- docs/conf.py | 2 +- docs/index.rst | 8 +++----- docs/requirements.txt | 8 ++++---- requirements.txt | 12 ++++++------ setup.py | 3 +-- tastytrade/streamer.py | 4 ++-- 6 files changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 88e90fd..00de59c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'tastytrade' copyright = '2023, Graeme Holliday' author = 'Graeme Holliday' -release = '1.0' +release = '5.7' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/index.rst b/docs/index.rst index 3e19688..98b0dd0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,15 +1,13 @@ tastytrade: An unofficial Python SDK for Tastytrade! ==================================================== +.. include:: ../README.rst + :start-after: inclusion-marker + .. toctree:: :maxdepth: 2 :caption: Documentation: -`View this project on GitHub `_ - -.. include:: ../README.rst - :start-after: inclusion-marker - .. toctree:: :maxdepth: 2 :caption: SDK Reference: diff --git a/docs/requirements.txt b/docs/requirements.txt index 4aee060..8947c51 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ -sphinx==5.3.0 -sphinx-rtd-theme==1.1.1 +sphinx==6.2.1 +sphinx-rtd-theme==1.2.2 sphinx-toolbox==3.4.0 -enum-tools==0.9.0.post1 -autodoc-pydantic==1.8.0 +enum-tools==0.10.0 +autodoc-pydantic==1.9.0 diff --git a/requirements.txt b/requirements.txt index f1a7de1..6efe945 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -requests==2.26.0 -mypy==0.931 -flake8==3.9.2 -isort==5.8.0 -types-requests==2.28.10 +requests==2.31.0 +mypy==1.4.1 +flake8==6.0.0 +isort==5.12.0 +types-requests==2.31.0.1 websockets==11.0.3 -pydantic==1.10.7 +pydantic==1.10.11 diff --git a/setup.py b/setup.py index cf5e2b1..0c0e185 100644 --- a/setup.py +++ b/setup.py @@ -19,9 +19,8 @@ license='MIT', install_requires=[ 'requests<3', - 'dataclasses', 'websockets>=11.0.3', - 'pydantic>=1.10.7' + 'pydantic<2' ], packages=find_packages(exclude=['ez_setup', 'tests*']), include_package_data=True diff --git a/tastytrade/streamer.py b/tastytrade/streamer.py index 5201738..916ecfa 100644 --- a/tastytrade/streamer.py +++ b/tastytrade/streamer.py @@ -182,7 +182,7 @@ async def account_subscribe(self, accounts: list[Account]) -> None: """ Subscribes to account-level updates (balances, orders, positions). - :param accounts: list of :class:`tastytrade.account.Account`s to subscribe to updates for + :param accounts: list of :class:`Account` to subscribe to updates for """ await self._subscribe(SubscriptionType.ACCOUNT, [acc.account_number for acc in accounts]) @@ -358,7 +358,7 @@ async def _handshake(self) -> None: async def listen(self) -> AsyncIterator[Event]: """ - Using the existing subscriptions, pulls :class:`~tastytrade.dxfeed.event.Event`s and yield returns + Using the existing subscriptions, pulls :class:`~tastytrade.dxfeed.event.Event` and yield returns them. Never exits unless there's an error or the channel is closed. """ while True: From b4adcbdd5332bcdfee0ac81f59a9a9ca9d865a5a Mon Sep 17 00:00:00 2001 From: Graeme22 Date: Wed, 5 Jul 2023 20:40:07 -0500 Subject: [PATCH 2/3] enforce line length (E501), add ETH address to funding --- .github/FUNDING.yml | 2 +- .github/workflows/python-app.yml | 2 +- Makefile | 2 +- tastytrade/account.py | 145 ++++++++++++++++++++----------- tastytrade/dxfeed/__init__.py | 3 +- tastytrade/dxfeed/event.py | 11 +-- tastytrade/dxfeed/greeks.py | 9 +- tastytrade/dxfeed/profile.py | 4 +- tastytrade/dxfeed/quote.py | 3 +- tastytrade/dxfeed/summary.py | 8 +- tastytrade/dxfeed/theoprice.py | 8 +- tastytrade/dxfeed/timeandsale.py | 15 ++-- tastytrade/dxfeed/trade.py | 6 +- tastytrade/instruments.py | 134 +++++++++++++++++----------- tastytrade/metrics.py | 17 ++-- tastytrade/order.py | 8 +- tastytrade/search.py | 6 +- tastytrade/session.py | 44 +++++++--- tastytrade/streamer.py | 143 ++++++++++++++++++++---------- tastytrade/watchlists.py | 58 ++++++++++--- 20 files changed, 423 insertions(+), 205 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d75d37b..b8b5c97 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ # If you want to support this project, you can just sign up using my referral code! -custom: ['https://start.tastytrade.com/#/login?referralCode=YYGKBEQ2EX'] +custom: ['https://start.tastytrade.com/#/login?referralCode=YYGKBEQ2EX', 'https://graemeholliday.argent.xyz'] diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 7bdc8bb..8171942 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -24,6 +24,6 @@ jobs: - name: Sorting... run: isort --check --diff tastytrade/ - name: Linting... - run: flake8 --count --show-source --statistics --ignore=E501 tastytrade/ + run: flake8 --count --show-source --statistics tastytrade/ - name: Type checking... run: mypy -p tastytrade diff --git a/Makefile b/Makefile index c07cb1a..be7d47a 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ venv: test: isort --check --diff tastytrade/ - flake8 --count --show-source --statistics --ignore=E501 tastytrade/ + flake8 --count --show-source --statistics tastytrade/ mypy -p tastytrade install: diff --git a/tastytrade/account.py b/tastytrade/account.py index 99c1f16..a185200 100644 --- a/tastytrade/account.py +++ b/tastytrade/account.py @@ -113,7 +113,8 @@ class AccountBalanceSnapshot(TastytradeJsonDataclass): class CurrentPosition(TastytradeJsonDataclass): """ - Dataclass containing imformation about an individual position in a portfolio. + Dataclass containing imformation about an individual position in a + portfolio. """ account_number: str symbol: str @@ -391,7 +392,11 @@ class Account(TastytradeJsonDataclass): submitting_user_id: Optional[str] = None @classmethod - def get_accounts(cls, session: Session, include_closed=False) -> list['Account']: + def get_accounts( + cls, + session: Session, + include_closed=False + ) -> list['Account']: """ Gets all trading accounts from the Tastyworks platform. By default excludes closed accounts from the results. @@ -445,7 +450,7 @@ def get_trading_status(self, session: Session) -> TradingStatus: :return: a Tastytrade 'TradingStatus' object in JSON format. """ response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/trading-status', + f'{session.base_url}/accounts/{self.account_number}/trading-status', # noqa: E501 headers=session.headers ) validate_response(response) # throws exception if not 200 @@ -477,17 +482,20 @@ def get_balance_snapshots( time_of_day: Optional[str] = None ) -> list[AccountBalanceSnapshot]: """ - Returns a list of two balance snapshots. The first one is the specified date, - or, if not provided, the oldest snapshot available. The second one is the most - recent snapshot. + Returns a list of two balance snapshots. The first one is the + specified date, or, if not provided, the oldest snapshot available. + The second one is the most recent snapshot. - If you provide the snapshot date, you must also provide the time of day. + If you provide the snapshot date, you must also provide the time of + day. :param session: the session to use for the request. :param snapshot_date: the date of the snapshot to get. - :param time_of_day: the time of day of the snapshot to get, either 'EOD' or 'BOD'. + :param time_of_day: + the time of day of the snapshot to get, either 'EOD' or 'BOD'. - :return: a list of two Tastytrade 'AccountBalanceSnapshot' objects in JSON format. + :return: + a list of two Tastytrade 'AccountBalanceSnapshot' in JSON format. """ params: dict[str, Any] = { 'snapshot-date': snapshot_date, @@ -495,7 +503,7 @@ def get_balance_snapshots( } response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/balance-snapshots', + f'{session.base_url}/accounts/{self.account_number}/balance-snapshots', # noqa: E501 headers=session.headers, params={k: v for k, v in params.items() if v is not None} ) @@ -521,14 +529,18 @@ def get_positions( Get the current positions of the account. :param session: the session to use for the request. - :param underlying_symbols: an array of underlying symbols for positions. + :param underlying_symbols: + an array of underlying symbols for positions. :param symbol: a single symbol. :param instrument_type: the type of instrument. - :param include_closed: if closed positions should be included in the query. + :param include_closed: + if closed positions should be included in the query. :param underlying_product_code: the underlying future's product code. :param partition_keys: account partition keys. - :param net_positions: returns net positions grouped by instrument type and symbol. - :param include_marks: include current quote mark (note: can decrease performance). + :param net_positions: + returns net positions grouped by instrument type and symbol. + :param include_marks: + include current quote mark (note: can decrease performance). :return: a list of Tastytrade 'CurrentPosition' objects in JSON format. """ @@ -578,7 +590,8 @@ def get_history( :param session: the session to use for the request. :param per_page: the number of results to return per page. - :param page_offset: provide a specific page to get; if not provided, get all pages + :param page_offset: + provide a specific page to get; if not provided, get all pages :param sort: the order to sort results in, either 'Desc' or 'Asc'. :param type: the type of transaction. :param types: a list of transaction types to filter by. @@ -589,12 +602,14 @@ def get_history( :param symbol: a single symbol. :param underlying_symbol: the underlying symbol. :param action: - the action of the transaction: 'Sell to Open', 'Sell to Close', 'Buy to Open', - 'Buy to Close', 'Sell' or 'Buy'. + the action of the transaction: 'Sell to Open', 'Sell to Close', + 'Buy to Open', 'Buy to Close', 'Sell' or 'Buy'. :param partition_key: account partition key. :param futures_symbol: the full TW Future Symbol, e.g. /ESZ9, /NGZ19. - :param start_at: datetime start range for filtering transactions in full date-time. - :param end_at: datetime end range for filtering transactions in full date-time. + :param start_at: + datetime start range for filtering transactions in full date-time. + :param end_at: + datetime end range for filtering transactions in full date-time. :return: a list of Tastytrade 'Transaction' objects in JSON format. """ @@ -627,7 +642,7 @@ def get_history( results = [] while True: response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/transactions', + f'{session.base_url}/accounts/{self.account_number}/transactions', # noqa: E501 headers=session.headers, params={k: v for k, v in params.items() if v is not None} ) @@ -655,7 +670,7 @@ def get_transaction(self, session: Session, id: int) -> Transaction: :return: a Tastytrade 'Transaction' object in JSON format. """ response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/transactions/{id}', + f'{session.base_url}/accounts/{self.account_number}/transactions/{id}', # noqa: E501 headers=session.headers ) validate_response(response) @@ -664,7 +679,11 @@ def get_transaction(self, session: Session, id: int) -> Transaction: return Transaction(**data) - def get_total_fees(self, session: Session, date: date = date.today()) -> dict[str, Any]: + def get_total_fees( + self, + session: Session, + date: date = date.today() + ) -> dict[str, Any]: """ Get the total fees for a given date. @@ -675,7 +694,7 @@ def get_total_fees(self, session: Session, date: date = date.today()) -> dict[st """ params: dict[str, Any] = {'date': date} response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/transactions/total-fees', + f'{session.base_url}/accounts/{self.account_number}/transactions/total-fees', # noqa: E501 headers=session.headers, params=params ) @@ -690,30 +709,35 @@ def get_net_liquidating_value_history( start_time: Optional[datetime] = None ) -> list[NetLiqOhlc]: """ - Returns a list of account net liquidating value snapshots over the specified time period. + Returns a list of account net liquidating value snapshots over the + specified time period. :param session: the session to use for the request. :param time_back: - the time period to get net liquidating value snapshots for. This param is required - if start_time is not given. Possible values are: '1d', '1m', '3m', '6m', '1y', 'all'. + the time period to get net liquidating value snapshots for. This + param is required if start_time is not given. Possible values are: + '1d', '1m', '3m', '6m', '1y', 'all'. :param start_time: - the start point for the query. This param is required is time-back is not given. - If given, will take precedence over time-back. + the start point for the query. This param is required is time-back + is not given. If given, will take precedence over time-back. :return: a list of Tastytrade 'NetLiqOhlc' objects in JSON format. """ params: dict[str, Any] = {} if start_time: # format to Tastytrade DateTime format - start_time = str(start_time).replace(' ', 'T').split('.')[0] + 'Z' # type: ignore + start_time = str(start_time) \ + .replace(' ', 'T') \ + .split('.')[0] + 'Z' # type: ignore params = {'start-time': start_time} elif not time_back: - raise TastytradeError('Either time_back or start_time must be specified.') + msg = 'Either time_back or start_time must be specified.' + raise TastytradeError(msg) else: params = {'time-back': time_back} response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/net-liq/history', + f'{session.base_url}/accounts/{self.account_number}/net-liq/history', # noqa: E501 headers=session.headers, params=params ) @@ -732,7 +756,7 @@ def get_position_limit(self, session: Session) -> PositionLimit: :return: a Tastytrade 'PositionLimit' object in JSON format. """ response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/position-limit', + f'{session.base_url}/accounts/{self.account_number}/position-limit', # noqa: E501 headers=session.headers ) validate_response(response) @@ -741,7 +765,11 @@ def get_position_limit(self, session: Session) -> PositionLimit: return PositionLimit(**data) - def get_effective_margin_requirements(self, session: Session, symbol: str) -> MarginRequirement: + def get_effective_margin_requirements( + self, + session: Session, + symbol: str + ) -> MarginRequirement: """ Get the effective margin requirements for a given symbol. @@ -753,7 +781,7 @@ def get_effective_margin_requirements(self, session: Session, symbol: str) -> Ma if symbol: symbol = symbol.replace('/', '%2F') response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/margin-requirements/{symbol}/effective', + f'{session.base_url}/accounts/{self.account_number}/margin-requirements/{symbol}/effective', # noqa: E501 headers=session.headers ) validate_response(response) @@ -764,15 +792,15 @@ def get_effective_margin_requirements(self, session: Session, symbol: str) -> Ma def get_margin_requirements(self, session: Session) -> MarginReport: """ - Get the margin report for the account, with total margin requirements as well - as a breakdown per symbol/instrument. + Get the margin report for the account, with total margin requirements + as well as a breakdown per symbol/instrument. :param session: the session to use for the request. :return: a :class:`MarginReport` object. """ response = requests.get( - f'{session.base_url}/margin/accounts/{self.account_number}/requirements', + f'{session.base_url}/margin/accounts/{self.account_number}/requirements', # noqa: E501 headers=session.headers ) validate_response(response) @@ -808,7 +836,7 @@ def get_order(self, session: Session, order_id: str) -> PlacedOrder: :return: an :class:`Order` object corresponding to the given ID. """ response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/orders/{order_id}', + f'{session.base_url}/accounts/{self.account_number}/orders/{order_id}', # noqa: E501 headers=session.headers ) validate_response(response) @@ -825,7 +853,7 @@ def delete_order(self, session: Session, order_id: str) -> None: :param order_id: the ID of the order to delete. """ response = requests.delete( - f'{session.base_url}/accounts/{self.account_number}/orders/{order_id}', + f'{session.base_url}/accounts/{self.account_number}/orders/{order_id}', # noqa: E501 headers=session.headers ) validate_response(response) @@ -850,16 +878,20 @@ def get_order_history( :param session: the session to use for the request. :param per_page: the number of results to return per page. - :param page_offset: provide a specific page to get; if not provided, get all pages + :param page_offset: + provide a specific page to get; if not provided, get all pages :param start_date: the start date of orders to query. :param end_date: the end date of orders to query. :param underlying_symbol: underlying symbol to filter by. :param statuses: a list of statuses to filter by. - :param futures_symbol: Tastytrade future symbol for futures and future options. - :param underlying_instrument_type: the type of instrument to filter by. + :param futures_symbol: + Tastytrade future symbol for futures and future options. + :param underlying_instrument_type: the type of instrument to filter by :param sort: the order to sort results in, either 'Desc' or 'Asc'. - :param start_at: datetime start range for filtering transactions in full date-time. - :param end_at: datetime end range for filtering transactions in full date-time. + :param start_at: + datetime start range for filtering transactions in full date-time. + :param end_at: + datetime end range for filtering transactions in full date-time. :return: a list of Tastytrade 'Transaction' objects in JSON format. """ @@ -905,7 +937,12 @@ def get_order_history( return [PlacedOrder(**entry) for entry in results] - def place_order(self, session: Session, order: NewOrder, dry_run=True) -> PlacedOrderResponse: + def place_order( + self, + session: Session, + order: NewOrder, + dry_run=True + ) -> PlacedOrderResponse: """ Place the given order. @@ -930,9 +967,15 @@ def place_order(self, session: Session, order: NewOrder, dry_run=True) -> Placed return PlacedOrderResponse(**data) - def replace_order(self, session: Session, old_order_id: str, new_order: NewOrder) -> PlacedOrder: + def replace_order( + self, + session: Session, + old_order_id: str, + new_order: NewOrder + ) -> PlacedOrder: """ - Replace an order with a new order with different characteristics (but same legs). + Replace an order with a new order with different characteristics (but + same legs). :param session: the session to use for the request. :param old_order_id: the ID of the order to replace. @@ -944,9 +987,13 @@ def replace_order(self, session: Session, old_order_id: str, new_order: NewOrder # required because we're passing the JSON as a string headers['Content-Type'] = 'application/json' response = requests.put( - f'{session.base_url}/accounts/{self.account_number}/orders/{old_order_id}', + f'{session.base_url}/accounts/{self.account_number}/orders/{old_order_id}', # noqa: E501 headers=headers, - data=new_order.json(exclude={'legs'}, exclude_none=True, by_alias=True) + data=new_order.json( + exclude={'legs'}, + exclude_none=True, + by_alias=True + ) ) validate_response(response) diff --git a/tastytrade/dxfeed/__init__.py b/tastytrade/dxfeed/__init__.py index 10f0140..36dfd2c 100644 --- a/tastytrade/dxfeed/__init__.py +++ b/tastytrade/dxfeed/__init__.py @@ -3,7 +3,8 @@ class Channel(str, Enum): """ - This is an :class:`~enum.Enum` that contains the channels for the quote streamer. + This is an :class:`~enum.Enum` that contains the channels for the quote + streamer. """ CANDLE = '/service/timeSeriesData' DATA = '/service/data' diff --git a/tastytrade/dxfeed/event.py b/tastytrade/dxfeed/event.py index 4a7fb33..0cf3839 100644 --- a/tastytrade/dxfeed/event.py +++ b/tastytrade/dxfeed/event.py @@ -8,7 +8,7 @@ class EventType(str, Enum): the quote streamer. Information on different types of events, their uses and their properties can be - found at the `dxfeed Knowledge Base `_. + found at the `dxfeed Knowledge Base `_. # noqa: E501 """ CANDLE = 'Candle' GREEKS = 'Greeks' @@ -24,18 +24,19 @@ class Event(ABC): @classmethod def from_stream(cls, data: list) -> list['Event']: """ - Takes a list of raw trade data fetched by :class:`~tastyworks.streamer.DataStreamer` - and returns a list of :class:`~tastyworks.dxfeed.event.Event` objects. + Makes a list of event objects from a list of raw trade data fetched by + a :class:`~tastyworks.streamer.DataStreamer`. :param data: list of raw quote data from streamer - :return: list of :class:`~tastyworks.dxfeed.event.Event` objects from data + :return: list of event objects from data """ objs = [] size = len(cls.__dataclass_fields__) # type: ignore multiples = len(data) / size if not multiples.is_integer(): - raise Exception('Mapper data input values are not an integer multiple of the key size') + msg = 'Mapper data input values are not a multiple of the key size' + raise Exception(msg) for i in range(int(multiples)): offset = i * size local_values = data[offset:(i + 1) * size] diff --git a/tastytrade/dxfeed/greeks.py b/tastytrade/dxfeed/greeks.py index 387cc06..f414199 100644 --- a/tastytrade/dxfeed/greeks.py +++ b/tastytrade/dxfeed/greeks.py @@ -6,7 +6,12 @@ @dataclass class Greeks(Event): """ - Greek ratios, or simply Greeks, are differential values that show how the price of an option depends on other market parameters: on the price of the underlying asset, its volatility, etc. Greeks are used to assess the risks of customer portfolios. Greeks are derivatives of the value of securities in different axes. If a derivative is very far from zero, then the portfolio has a risky sensitivity in this parameter. + Greek ratios, or simply Greeks, are differential values that show how the + price of an option depends on other market parameters: on the price of the + underlying asset, its volatility, etc. Greeks are used to assess the risks + of customer portfolios. Greeks are derivatives of the value of securities + in different axes. If a derivative is very far from zero, then the + portfolio has a risky sensitivity in this parameter. """ #: symbol of this event eventSymbol: str @@ -18,7 +23,7 @@ class Greeks(Event): index: int #: timestamp of this event in milliseconds time: int - #: sequence number of thie event to distinguish events that have the same time + #: sequence number to distinguish events that have the same time sequence: int #: option market price price: float diff --git a/tastytrade/dxfeed/profile.py b/tastytrade/dxfeed/profile.py index 1ba34f0..1bd8af9 100644 --- a/tastytrade/dxfeed/profile.py +++ b/tastytrade/dxfeed/profile.py @@ -6,7 +6,9 @@ @dataclass class Profile(Event): """ - A Profile event provides the security instrument description. It represents the most recent information that is available about the traded security on the market at any given moment of time. + A Profile event provides the security instrument description. It + represents the most recent information that is available about the + traded security on the market at any given moment of time. """ #: symbol of this event eventSymbol: str diff --git a/tastytrade/dxfeed/quote.py b/tastytrade/dxfeed/quote.py index fae69c8..5d9a107 100644 --- a/tastytrade/dxfeed/quote.py +++ b/tastytrade/dxfeed/quote.py @@ -6,7 +6,8 @@ @dataclass class Quote(Event): """ - A Quote event is a snapshot of the best bid and ask prices, and other fields that change with each quote. + A Quote event is a snapshot of the best bid and ask prices, and other + fields that change with each quote. """ #: symbol of this event eventSymbol: str diff --git a/tastytrade/dxfeed/summary.py b/tastytrade/dxfeed/summary.py index a76f099..7bbe617 100644 --- a/tastytrade/dxfeed/summary.py +++ b/tastytrade/dxfeed/summary.py @@ -6,11 +6,13 @@ @dataclass class Summary(Event): """ - Summary is an information snapshot about the trading session including session highs, lows, etc. This record has two goals: + Summary is an information snapshot about the trading session including + session highs, lows, etc. This record has two goals: 1. Transmit OHLC values. - - 2. Provide data for charting. OHLC is required for a daily chart, and if an exchange does not provide it, the charting services refer to the Summary event. + 2. Provide data for charting. OHLC is required for a daily chart, and + if an exchange does not provide it, the charting services refer to the + Summary event. Before opening the bidding, the values are reset to N/A or NaN. """ diff --git a/tastytrade/dxfeed/theoprice.py b/tastytrade/dxfeed/theoprice.py index e4e22b1..bb52219 100644 --- a/tastytrade/dxfeed/theoprice.py +++ b/tastytrade/dxfeed/theoprice.py @@ -6,7 +6,11 @@ @dataclass class TheoPrice(Event): """ - Theo price is a snapshot of the theoretical option price computation that is periodically performed by dxPrice model-free computation. dxFeed does not send recalculations for all options at the same time, so we provide you with a formula so you can perform calculations based on values from this event. + Theo price is a snapshot of the theoretical option price computation that + is periodically performed by dxPrice model-free computation. dxFeed does + not send recalculations for all options at the same time, so we provide + you with a formula so you can perform calculations based on values from + this event. """ #: symbol of this event eventSymbol: str @@ -18,7 +22,7 @@ class TheoPrice(Event): index: int #: timestamp of this event in milliseconds time: int - #: sequence number of this event to distinguish events that have the same time + #: sequence number to distinguish events that have the same time sequence: int #: theoretical price price: float diff --git a/tastytrade/dxfeed/timeandsale.py b/tastytrade/dxfeed/timeandsale.py index 6bd0e5a..2d27034 100644 --- a/tastytrade/dxfeed/timeandsale.py +++ b/tastytrade/dxfeed/timeandsale.py @@ -6,11 +6,12 @@ @dataclass class TimeAndSale(Event): """ - TimeAndSale event represents a trade or other market event with a price, like - market open/close price. TimeAndSale events are intended to provide information - about trades in a continuous-time slice (unlike Trade events which are supposed - to provide snapshots about the most recent trade). TimeAndSale events have a - unique index that can be used for later correction/cancellation processing. + TimeAndSale event represents a trade or other market event with a price, + like market open/close price. TimeAndSale events are intended to provide + information about trades in a continuous-time slice (unlike Trade events + which are supposed to provide snapshots about the most recent trade). + TimeAndSale events have a unique index that can be used for later + correction/cancellation processing. """ #: symbol of this event eventSymbol: str @@ -32,9 +33,9 @@ class TimeAndSale(Event): price: float #: size of this time and sale event as integer number (rounded toward zero) size: int - #: the current bid price on the market when this time and sale event had occured + #: the bid price on the market when this time and sale event occured bidPrice: float - #: the current ask price on the market when this time and sale event had occured + #: the ask price on the market when this time and sale event occured askPrice: float #: sale conditions provided for this event by data feed exchangeSaleConditions: str diff --git a/tastytrade/dxfeed/trade.py b/tastytrade/dxfeed/trade.py index fae1fa4..33a3dd3 100644 --- a/tastytrade/dxfeed/trade.py +++ b/tastytrade/dxfeed/trade.py @@ -6,7 +6,11 @@ @dataclass class Trade(Event): """ - A Trade event provides prices and the volume of the last transaction in regular trading hours, as well as the total amount per day in the number of securities and in their value. This event does not contain information about all transactions, but only about the last transaction for a single instrument. + A Trade event provides prices and the volume of the last transaction in + regular trading hours, as well as the total amount per day in the number + of securities and in their value. This event does not contain information + about all transactions, but only about the last transaction for a single + instrument. """ #: symbol of this event eventSymbol: str diff --git a/tastytrade/instruments.py b/tastytrade/instruments.py index 08c3c42..4e7b035 100644 --- a/tastytrade/instruments.py +++ b/tastytrade/instruments.py @@ -12,8 +12,8 @@ class OptionType(str, Enum): """ - This is an :class:`~enum.Enum` that contains the valid types of options and - their abbreviations in the API. + This is an :class:`~enum.Enum` that contains the valid types of options + and their abbreviations in the API. """ CALL = 'C' PUT = 'P' @@ -21,9 +21,10 @@ class OptionType(str, Enum): class FutureMonthCode(str, Enum): """ - This is an :class:`~enum.Enum` that contains the valid month codes for futures. + This is an :class:`~enum.Enum` that contains the valid month codes for + futures. - This is really just here for reference, as the API barely uses these codes. + This is really here for reference, as the API barely uses these codes. """ JAN = 'F' FEB = 'G' @@ -55,7 +56,8 @@ class Deliverable(TastytradeJsonDataclass): class DestinationVenueSymbol(TastytradeJsonDataclass): """ - Dataclass representing a specific destination venue symbol for a cryptocurrency. + Dataclass representing a specific destination venue symbol for a + cryptocurrency. """ id: int symbol: str @@ -67,7 +69,8 @@ class DestinationVenueSymbol(TastytradeJsonDataclass): class QuantityDecimalPrecision(TastytradeJsonDataclass): """ - Dataclass representing the decimal precision (number of places) for an instrument. + Dataclass representing the decimal precision (number of places) for an + instrument. """ instrument_type: InstrumentType value: int @@ -77,8 +80,8 @@ class QuantityDecimalPrecision(TastytradeJsonDataclass): class Strike(TastytradeJsonDataclass): """ - Dataclass representing a specific strike in an options chain, containing the - symbols for the call and put options. + Dataclass representing a specific strike in an options chain, containing + the symbols for the call and put options. """ strike_price: Decimal call: str @@ -129,7 +132,8 @@ class NestedFutureOptionChainExpiration(TastytradeJsonDataclass): class NestedFutureOptionFuture(TastytradeJsonDataclass): """ - Dataclass representing an underlying future in a nested future options chain. + Dataclass representing an underlying future in a nested future options + chain. """ root_symbol: str days_to_expiration: int @@ -182,12 +186,12 @@ def get_cryptocurrencies( cls, session: Session, symbols: list[str] = [] ) -> list['Cryptocurrency']: """ - Returns a list of :class:`Cryptocurrency` objects from the given symbols. + Returns a list of cryptocurrency objects from the given symbols. :param session: the session to use for the request. :param symbols: the symbols to get the cryptocurrencies for. - :return: a list of :class:`Cryptocurrency` objects. + :return: a list of cryptocurrency objects. """ params = {'symbol[]': symbols} if symbols else None response = requests.get( @@ -202,7 +206,11 @@ def get_cryptocurrencies( return [cls(**entry) for entry in data] @classmethod - def get_cryptocurrency(cls, session: Session, symbol: str) -> 'Cryptocurrency': + def get_cryptocurrency( + cls, + session: Session, + symbol: str + ) -> 'Cryptocurrency': """ Returns a :class:`Cryptocurrency` object from the given symbol. @@ -262,8 +270,11 @@ def get_active_equities( :param session: the session to use for the request. :param per_page: the number of equities to get per page. - :param page_offset: provide a specific page to get; if not provided, get all pages - :param lendability: the lendability of the equities; 'Easy To Borrow', 'Locate Required', 'Preborrow' + :param page_offset: + provide a specific page to get; if not provided, get all pages + :param lendability: + the lendability of the equities; e.g. 'Easy To Borrow', + 'Locate Required', 'Preborrow' :return: a list of :class:`Equity` objects. """ @@ -317,7 +328,8 @@ def get_equities( :param session: the session to use for the request. :param symbols: the symbols to get the equities for. :param lendability: - the lendability of the equities; 'Easy To Borrow', 'Locate Required', 'Preborrow' + the lendability of the equities; e.g. 'Easy To Borrow', + 'Locate Required', 'Preborrow' :param is_index: whether the equities are indexes. :param is_etf: whether the equities are ETFs. @@ -470,12 +482,13 @@ def _set_streamer_symbol(self) -> None: class NestedOptionChain(TastytradeJsonDataclass): """ - Dataclass that represents a Tastytrade nested option chain object. Contains - information about the option chain and a method to fetch one for a symbol. + Dataclass that represents a Tastytrade nested option chain object. + Contains information about the option chain and a method to fetch one for + a symbol. - The nested option chain is a bit neater than calling :meth:`get_option_chain` but - if you want to create actual :class:`Option` objects you'll need to make an extra - API request or two. + This is cleaner than calling :meth:`get_option_chain` but if you want to + create actual :class:`Option` objects you'll need to make an extra API + request or two. """ underlying_symbol: str root_symbol: str @@ -510,7 +523,8 @@ def get_chain(cls, session: Session, symbol: str) -> 'NestedOptionChain': class FutureProduct(TastytradeJsonDataclass): """ Dataclass that represents a Tastytrade future product object. Contains - information about the future product and a method to fetch one for a symbol. + information about the future product and a method to fetch one for a + symbol. Useful for fetching general information about a family of futures, without knowing the specific expirations or symbols. @@ -579,13 +593,14 @@ def get_future_product( :param session: the session to use for the request. :param code: the product code, e.g. 'ES' - :param exchange: the exchange to get the product from: 'CME', 'SMALLS', 'CFE', 'CBOED' + :param exchange: + the exchange to fetch from: 'CME', 'SMALLS', 'CFE', 'CBOED' :return: a :class:`FutureProduct` object. """ code = code.replace('/', '') response = requests.get( - f'{session.base_url}/instruments/future-products/{exchange}/{code}', + f'{session.base_url}/instruments/future-products/{exchange}/{code}', # noqa: E501 headers=session.headers ) validate_response(response) @@ -597,8 +612,8 @@ def get_future_product( class Future(TradeableTastytradeJsonDataclass): """ - Dataclass that represents a Tastytrade future object. Contains information about - the future and methods to fetch futures for symbol(s). + Dataclass that represents a Tastytrade future object. Contains information + about the future and methods to fetch futures for symbol(s). """ product_code: str tick_size: Decimal @@ -640,13 +655,15 @@ def get_futures( product_codes: Optional[list[str]] = None ) -> list['Future']: """ - Returns a list of :class:`Future` objects from the given symbols or product codes. + Returns a list of :class:`Future` objects from the given symbols + or product codes. :param session: the session to use for the request. :param symbols: - symbols of the futures, e.g. 'ESZ9'. Leading forward slash is not required. + symbols of the futures, e.g. 'ESZ9', '/ESZ9'. :param product_codes: - the product codes of the futures, e.g. 'ES', '6A'. Ignored if symbols are provided. + the product codes of the futures, e.g. 'ES', '6A'. Ignored if + symbols are provided. :return: a list of :class:`Future` objects. """ @@ -689,8 +706,9 @@ def get_future(cls, session: Session, symbol: str) -> 'Future': class FutureOptionProduct(TastytradeJsonDataclass): """ - Dataclass that represents a Tastytrade future option product object. Contains - information about the future option product (deliverable for the future option). + Dataclass that represents a Tastytrade future option product object. + Contains information about the future option product (deliverable for + the future option). """ root_symbol: str cash_settled: bool @@ -750,7 +768,7 @@ def get_future_option_product( """ root_symbol = root_symbol.replace('/', '') response = requests.get( - f'{session.base_url}/instruments/future-option-products/{exchange}/{root_symbol}', + f'{session.base_url}/instruments/future-option-products/{exchange}/{root_symbol}', # noqa: E501 headers=session.headers ) validate_response(response) @@ -812,11 +830,13 @@ def get_future_options( """ Returns a list of :class:`FutureOption` objects from the given symbols. - NOTE: As far as I can tell, all of the parameters are bugged except for `symbols`. + NOTE: As far as I can tell, all of the parameters are bugged except + for `symbols`. :param session: the session to use for the request. :param symbols: the Tastytrade symbols to filter by. - :param root_symbol: the root symbol to get the future options for, e.g. 'EW3', 'SO' + :param root_symbol: + the root symbol to get the future options for, e.g. 'EW3', 'SO' :param expiration_date: the expiration date for the future options. :param option_type: the option type to filter by. :param strike_price: the strike price to filter by. @@ -883,15 +903,19 @@ class NestedFutureOptionChain(TastytradeJsonDataclass): Dataclass that represents a Tastytrade nested option chain object. Contains information about the option chain and a method to fetch one for a symbol. - The nested option chain is a bit neater than calling :meth:`get_future_option_chain` - but if you want to create actual :class:`FutureOption` objects you'll need to make an + This is cleaner than calling :meth:`get_future_option_chain` but if you + want to create actual :class:`FutureOption` objects you'll need to make an extra API request or two. """ futures: list[NestedFutureOptionFuture] option_chains: list[NestedFutureOptionSubchain] @classmethod - def get_chain(cls, session: Session, symbol: str) -> 'NestedFutureOptionChain': + def get_chain( + cls, + session: Session, + symbol: str + ) -> 'NestedFutureOptionChain': """ Gets the futures option chain for the given symbol in nested format. @@ -976,7 +1000,9 @@ def get_warrant(cls, session: Session, symbol: str) -> 'Warrant': FutureProduct.update_forward_refs() -def get_quantity_decimal_precisions(session: Session) -> list[QuantityDecimalPrecision]: +def get_quantity_decimal_precisions( + session: Session +) -> list[QuantityDecimalPrecision]: """ Returns a list of :class:`QuantityDecimalPrecision` objects for different types of instruments. @@ -996,19 +1022,23 @@ def get_quantity_decimal_precisions(session: Session) -> list[QuantityDecimalPre return [QuantityDecimalPrecision(**entry) for entry in data] -def get_option_chain(session: Session, symbol: str) -> dict[date, list[Option]]: +def get_option_chain( + session: Session, + symbol: str +) -> dict[date, list[Option]]: """ - Returns a mapping of expiration date to a list of :class:`Option` objects + Returns a mapping of expiration date to a list of option objects representing the options chain for the given symbol. - In the case that there are two expiries on the same day (e.g. SPXW and SPX AM - options), both will be returned in the same list. If you just want one expiry, - you'll need to filter the list yourself, or use ~:class:`NestedOptionChain` instead. + In the case that there are two expiries on the same day (e.g. SPXW + and SPX AM options), both will be returned in the same list. If you + just want one expiry, you'll need to filter the list yourself, or use + ~:class:`NestedOptionChain` instead. :param session: the session to use for the request. :param symbol: the symbol to get the option chain for. - :return: a dict mapping expiration date to a list of :class:`Option` objects. + :return: a dict mapping expiration date to a list of options """ symbol = symbol.replace('/', '%2F') response = requests.get( @@ -1029,19 +1059,23 @@ def get_option_chain(session: Session, symbol: str) -> dict[date, list[Option]]: return chain -def get_future_option_chain(session: Session, symbol: str) -> dict[date, list[FutureOption]]: +def get_future_option_chain( + session: Session, + symbol: str +) -> dict[date, list[FutureOption]]: """ - Returns a mapping of expiration date to a list of :class:`FutureOption` objects - representing the options chain for the given symbol. + Returns a mapping of expiration date to a list of futures options + objects representing the options chain for the given symbol. - In the case that there are two expiries on the same day (e.g. EW and ES options), - both will be returned in the same list. If you just want one expiry, you'll need - to filter the list yourself, or use ~:class:`NestedFutureOptionChain` instead. + In the case that there are two expiries on the same day (e.g. EW + and ES options), both will be returned in the same list. If you + just want one expiry, you'll need to filter the list yourself, or + use ~:class:`NestedFutureOptionChain` instead. :param session: the session to use for the request. :param symbol: the symbol to get the option chain for. - :return: a dict mapping expiration date to a list of :class:`FutureOption` objects. + :return: a dict mapping expiration date to a list of futures options. """ symbol = symbol.replace('/', '') response = requests.get( diff --git a/tastytrade/metrics.py b/tastytrade/metrics.py index fec9cd2..9fbef69 100644 --- a/tastytrade/metrics.py +++ b/tastytrade/metrics.py @@ -66,7 +66,7 @@ class MarketMetricInfo(TastytradeJsonDataclass): liquidity_rating: int created_at: datetime updated_at: datetime - option_expiration_implied_volatilities: list[OptionExpirationImpliedVolatility] + option_expiration_implied_volatilities: list[OptionExpirationImpliedVolatility] # noqa: E501 liquidity_running_state: Liquidity beta: Decimal beta_updated_at: datetime @@ -90,7 +90,10 @@ class MarketMetricInfo(TastytradeJsonDataclass): dividend_updated_at: Optional[datetime] = None -def get_market_metrics(session: Session, symbols: list[str]) -> list[MarketMetricInfo]: +def get_market_metrics( + session: Session, + symbols: list[str] +) -> list[MarketMetricInfo]: """ Retrieves market metrics for the given symbols. @@ -122,7 +125,7 @@ def get_dividends(session: Session, symbol: str) -> list[DividendInfo]: """ symbol = symbol.replace('/', '%2F') response = requests.get( - f'{session.base_url}/market-metrics/historic-corporate-events/dividends/{symbol}', + f'{session.base_url}/market-metrics/historic-corporate-events/dividends/{symbol}', # noqa: E501 headers=session.headers ) validate_response(response) @@ -132,7 +135,11 @@ def get_dividends(session: Session, symbol: str) -> list[DividendInfo]: return [DividendInfo(**entry) for entry in data] -def get_earnings(session: Session, symbol: str, start_date: date) -> list[EarningsInfo]: +def get_earnings( + session: Session, + symbol: str, + start_date: date +) -> list[EarningsInfo]: """ Retrieves earnings information for the given symbol. @@ -145,7 +152,7 @@ def get_earnings(session: Session, symbol: str, start_date: date) -> list[Earnin symbol = symbol.replace('/', '%2F') params: dict[str, Any] = {'start-date': start_date} response = requests.get( - f'{session.base_url}/market-metrics/historic-corporate-events/earnings-reports/{symbol}', + f'{session.base_url}/market-metrics/historic-corporate-events/earnings-reports/{symbol}', # noqa: E501 headers=session.headers, params=params ) diff --git a/tastytrade/order.py b/tastytrade/order.py index 479542b..66b7e27 100644 --- a/tastytrade/order.py +++ b/tastytrade/order.py @@ -342,7 +342,8 @@ class OrderChainEntry(TastytradeJsonDataclass): class OrderChainLeg(TastytradeJsonDataclass): """ - Dataclass containing information about a single leg in an order from an order chain. + Dataclass containing information about a single leg in an order + from an order chain. """ symbol: str instrument_type: InstrumentType @@ -409,8 +410,9 @@ class ComputedData(TastytradeJsonDataclass): class OrderChain(TastytradeJsonDataclass): """ - Dataclass containing information about an order chain: a group of orders for a - specific underlying, such as total P/L, rolls, current P/L in a symbol, etc. + Dataclass containing information about an order chain: a group of orders + for a specific underlying, such as total P/L, rolls, current P/L in a + symbol, etc. """ id: int updated_at: datetime diff --git a/tastytrade/search.py b/tastytrade/search.py index 50841a3..17af83d 100644 --- a/tastytrade/search.py +++ b/tastytrade/search.py @@ -14,8 +14,8 @@ class SymbolData(TastytradeJsonDataclass): def symbol_search(session: Session, symbol: str) -> list[SymbolData]: """ - Performs a symbol search using the Tastytrade API and returns a list of symbols that - are similar to the given search phrase. + Performs a symbol search using the Tastytrade API and returns a + list of symbols that are similar to the given search phrase. :param session: active user session to use :param symbol: search phrase @@ -28,7 +28,7 @@ def symbol_search(session: Session, symbol: str) -> list[SymbolData]: headers=session.headers ) if response.status_code // 100 != 2: - # here it doesn't really make sense to throw an exception; we'll just return nothing + # here it doesn't really make sense to throw an exception return [] else: data = response.json()['data']['items'] diff --git a/tastytrade/session.py b/tastytrade/session.py index 9996721..3707071 100644 --- a/tastytrade/session.py +++ b/tastytrade/session.py @@ -8,15 +8,18 @@ class Session: """ - Contains a local user login which can then be used to interact with the remote API. + Contains a local user login which can then be used to interact with the + remote API. :param login: tastytrade username or email - :param password: tastytrade password or a remember token obtained previously + :param password: + tastytrade password or a remember token obtained previously :param remember_me: - whether or not to create a single-use remember token to use in place of a password; - currently appears to be bugged. + whether or not to create a single-use remember token to use in place + of a password; currently appears to be bugged. :param two_factor_authentication: - if two factor authentication is enabled, this is the code sent to the user's device + if two factor authentication is enabled, this is the code sent to the + user's device :param is_certification: whether or not to use the certification API """ def __init__( @@ -39,7 +42,11 @@ def __init__( if two_factor_authentication: headers = {'X-Tastyworks-OTP': two_factor_authentication} - response = requests.post(f'{self.base_url}/sessions', json=body, headers=headers) + response = requests.post( + f'{self.base_url}/sessions', + json=body, + headers=headers + ) else: response = requests.post(f'{self.base_url}/sessions', json=body) validate_response(response) # throws exception if not 200 @@ -50,7 +57,8 @@ def __init__( #: The session token used to authenticate requests self.session_token: str = json['data']['session-token'] #: A single-use token which can be used to login without a password - self.remember_token: Optional[str] = json['data']['remember-token'] if remember_me else None + self.remember_token: Optional[str] = \ + json['data']['remember-token'] if remember_me else None #: The headers to use for API requests self.headers: dict[str, str] = {'Authorization': self.session_token} self.validate() @@ -61,17 +69,24 @@ def validate(self) -> bool: :return: True if the session is valid and False otherwise. """ - response = requests.post(f'{self.base_url}/sessions/validate', headers=self.headers) + response = requests.post( + f'{self.base_url}/sessions/validate', + headers=self.headers + ) return (response.status_code // 100 == 2) def destroy(self) -> bool: """ - Sends a API request to log out of the existing session. This will invalidate the - current session token and login. + Sends a API request to log out of the existing session. This will + invalidate the current session token and login. - :return: True if the session was terminated successfully and False otherwise. + :return: True if the session was terminated successfully and + False otherwise. """ - response = requests.delete(f'{self.base_url}/sessions', headers=self.headers) + response = requests.delete( + f'{self.base_url}/sessions', + headers=self.headers + ) return (response.status_code // 100 == 2) def get_customer(self) -> dict[str, Any]: @@ -80,7 +95,10 @@ def get_customer(self) -> dict[str, Any]: :return: a Tastytrade 'Customer' object in JSON format. """ - response = requests.get(f'{self.base_url}/customers/me', headers=self.headers) + response = requests.get( + f'{self.base_url}/customers/me', + headers=self.headers + ) validate_response(response) # throws exception if not 200 return response.json()['data'] diff --git a/tastytrade/streamer.py b/tastytrade/streamer.py index 916ecfa..8207f65 100644 --- a/tastytrade/streamer.py +++ b/tastytrade/streamer.py @@ -53,7 +53,8 @@ class QuoteAlert(TastytradeJsonDataclass): class UnderlyingYearGainSummary(TastytradeJsonDataclass): """ - Dataclass that contains information about the yearly gainYloss for an underlying + Dataclass that contains information about the yearly gain + or loss for an underlying """ year: int account_number: str @@ -71,9 +72,10 @@ class UnderlyingYearGainSummary(TastytradeJsonDataclass): class SubscriptionType(str, Enum): """ - This is an :class:`~enum.Enum` that contains the subscription types for the alert streamer. + This is an :class:`~enum.Enum` that contains the subscription types + for the alert streamer. """ - ACCOUNT = 'account-subscribe' # 'account-subscribe' may be deprecated in the future + ACCOUNT = 'account-subscribe' # may be 'connect' in the future HEARTBEAT = 'heartbeat' PUBLIC_WATCHLISTS = 'public-watchlists-subscribe' QUOTE_ALERTS = 'quote-alerts-subscribe' @@ -82,10 +84,10 @@ class SubscriptionType(str, Enum): class AlertStreamer: """ - Used to subscribe to account-level updates (balances, orders, positions), public - watchlist updates, quote alerts, and user-level messages. It should always be - initialized using the :meth:`create` function, since the object cannot be fully - instantiated without using async. + Used to subscribe to account-level updates (balances, orders, positions), + public watchlist updates, quote alerts, and user-level messages. It should + always be initialized using the :meth:`create` function, since the object + cannot be fully instantiated without using async. Example usage:: @@ -105,7 +107,8 @@ def __init__(self, session: Session): #: The active session used to initiate the streamer or make requests self.token: str = session.session_token #: The base url for the streamer websocket - self.base_url: str = CERT_STREAMER_URL if session.is_certification else STREAMER_URL + self.base_url: str = \ + CERT_STREAMER_URL if session.is_certification else STREAMER_URL self._queue: Queue = Queue() self._websocket = None @@ -116,8 +119,8 @@ def __init__(self, session: Session): async def create(cls, session: Session) -> 'AlertStreamer': """ Factory method for the :class:`DataStreamer` object. Simply calls the - constructor and performs the asynchronous setup tasks. This should be used - instead of the constructor. + constructor and performs the asynchronous setup tasks. This should be + used instead of the constructor. :param session: active user session to use """ @@ -129,11 +132,14 @@ async def create(cls, session: Session) -> 'AlertStreamer': async def _connect(self) -> None: """ - Connect to the websocket server using the URL and authorization token provided - during initialization. + Connect to the websocket server using the URL and authorization + token provided during initialization. """ headers = {'Authorization': f'Bearer {self.token}'} - async with websockets.connect(self.base_url, extra_headers=headers) as websocket: # type: ignore + async with websockets.connect( # type: ignore + self.base_url, + extra_headers=headers + ) as websocket: self._websocket = websocket self._heartbeat_task = asyncio.create_task(self._heartbeat()) @@ -155,9 +161,14 @@ async def listen(self) -> AsyncIterator[TastytradeJsonDataclass]: elif data.get('action') != 'heartbeat': logger.debug('subscription message: %s', data) - def _map_message(self, type_str: str, data: dict) -> TastytradeJsonDataclass: + def _map_message( + self, + type_str: str, + data: dict + ) -> TastytradeJsonDataclass: """ - I'm not sure what the user-status messages look like, so they're absent. + I'm not sure what the user-status messages look like, + so they're absent. """ if type_str == 'AccountBalance': return AccountBalance(**data) @@ -184,7 +195,10 @@ async def account_subscribe(self, accounts: list[Account]) -> None: :param accounts: list of :class:`Account` to subscribe to updates for """ - await self._subscribe(SubscriptionType.ACCOUNT, [acc.account_number for acc in accounts]) + await self._subscribe( + SubscriptionType.ACCOUNT, + [acc.account_number for acc in accounts] + ) async def public_watchlists_subscribe(self) -> None: """ @@ -214,16 +228,21 @@ def close(self) -> None: async def _heartbeat(self) -> None: """ - Sends a heartbeat message every 10 seconds to keep the connection alive. + Sends a heartbeat message every 10 seconds to keep the connection + alive. """ while True: await self._subscribe(SubscriptionType.HEARTBEAT, '') # send the heartbeat every 10 seconds await asyncio.sleep(10) - async def _subscribe(self, subscription: SubscriptionType, value: Union[Optional[str], list[str]] = '') -> None: + async def _subscribe( + self, + subscription: SubscriptionType, + value: Union[Optional[str], list[str]] = '' + ) -> None: """ - Subscribes to one of the :class:`SubscriptionType`s. Depending on the kind of + Subscribes to a :class:`SubscriptionType`. Depending on the kind of subscription, the value parameter may be required. """ message: dict[str, Any] = { @@ -238,9 +257,10 @@ async def _subscribe(self, subscription: SubscriptionType, value: Union[Optional class DataStreamer: """ - A :class:`DataStreamer` object is used to fetch quotes or greeks for a given symbol - or list of symbols. It should always be initialized using the :meth:`create` function, - since the object cannot be fully instantiated without using async. + A :class:`DataStreamer` object is used to fetch quotes or greeks + for a given symbol or list of symbols. It should always be + initialized using the :meth:`create` function, since the object + cannot be fully instantiated without using async. Example usage:: @@ -267,7 +287,10 @@ def __init__(self, session: Session): #: The unique client identifier received from the server self.client_id: Optional[str] = None - response = requests.get(f'{session.base_url}/quote-streamer-tokens', headers=session.headers) + response = requests.get( + f'{session.base_url}/quote-streamer-tokens', + headers=session.headers + ) validate_response(response) logger.debug('response %s', json.dumps(response.json())) self._auth_token = response.json()['data']['token'] @@ -279,9 +302,9 @@ def __init__(self, session: Session): @classmethod async def create(cls, session: Session) -> 'DataStreamer': """ - Factory method for the :class:`DataStreamer` object. Simply calls the - constructor and performs the asynchronous setup tasks. This should be used - instead of the constructor. + Factory method for the :class:`DataStreamer` object. + Simply calls the constructor and performs the asynchronous + setup tasks. This should be used instead of the constructor. Setup time is around 10-15 seconds. @@ -300,12 +323,15 @@ async def _next_id(self): async def _connect(self) -> None: """ - Connect to the websocket server using the URL and authorization token provided - during initialization. + Connect to the websocket server using the URL and + authorization token provided during initialization. """ headers = {'Authorization': f'Bearer {self._auth_token}'} - async with websockets.connect(self._wss_url, extra_headers=headers) as websocket: # type: ignore + async with websockets.connect( # type: ignore + self._wss_url, + extra_headers=headers + ) as websocket: self._websocket = websocket await self._handshake() @@ -317,7 +343,8 @@ async def _connect(self) -> None: if message['channel'] == Channel.HANDSHAKE: if message['successful']: self.client_id = message['clientId'] - self._heartbeat_task = asyncio.create_task(self._heartbeat()) + self._heartbeat_task = \ + asyncio.create_task(self._heartbeat()) else: raise TastytradeError('Handshake failed') @@ -337,8 +364,9 @@ async def _connect(self) -> None: async def _handshake(self) -> None: """ - Sends a handshake message to the specified WebSocket connection. The handshake - message is sent as a JSON-encoded array with a single element, containing the + Sends a handshake message to the specified WebSocket + connection. The handshake message is sent as a JSON + encoded array with a single element, containing the handshake message as its only element. """ id = await self._next_id() @@ -347,7 +375,11 @@ async def _handshake(self) -> None: 'version': '1.0', 'minimumVersion': '1.0', 'channel': Channel.HANDSHAKE, - 'supportedConnectionTypes': ['websocket', 'long-polling', 'callback-polling'], + 'supportedConnectionTypes': [ + 'websocket', + 'long-polling', + 'callback-polling' + ], 'ext': {'com.devexperts.auth.AuthToken': self._auth_token}, 'advice': { 'timeout': 60000, @@ -358,7 +390,7 @@ async def _handshake(self) -> None: async def listen(self) -> AsyncIterator[Event]: """ - Using the existing subscriptions, pulls :class:`~tastytrade.dxfeed.event.Event` and yield returns + Using the existing subscriptions, pulls events and yield returns them. Never exits unless there's an error or the channel is closed. """ while True: @@ -369,7 +401,8 @@ async def listen(self) -> AsyncIterator[Event]: async def listen_candle(self) -> AsyncIterator[Candle]: """ - Using the existing subscriptions, pulls candles and yield returns them. + Using the existing subscriptions, pulls candles and yield + returns them. Never exits unless there's an error or the channel is closed. """ while True: @@ -387,7 +420,8 @@ def close(self) -> None: async def _heartbeat(self) -> None: """ - Sends a heartbeat message every 10 seconds to keep the connection alive. + Sends a heartbeat message every 10 seconds to keep the connection + alive. """ while True: id = await self._next_id() @@ -402,14 +436,21 @@ async def _heartbeat(self) -> None: # send the heartbeat every 10 seconds await asyncio.sleep(10) - async def subscribe(self, event_type: EventType, symbols: list[str], reset: bool = False) -> None: + async def subscribe( + self, + event_type: EventType, + symbols: list[str], + reset: bool = False + ) -> None: """ - Subscribes to quotes for given list of symbols. Used for recurring data feeds. + Subscribes to quotes for given list of symbols. Used for recurring data + feeds. :param event_type: type of subscription to add :param symbols: list of symbols to subscribe for :param reset: - whether to reset the subscription list (remove all other subscriptions of all types) + whether to reset the subscription list (remove all other + subscriptions of all types) """ id = await self._next_id() message = { @@ -424,7 +465,11 @@ async def subscribe(self, event_type: EventType, symbols: list[str], reset: bool logger.debug('sending subscription: %s', message) await self._websocket.send(json.dumps([message])) - async def unsubscribe(self, event_type: EventType, symbols: list[str]) -> None: + async def unsubscribe( + self, + event_type: EventType, + symbols: list[str] + ) -> None: """ Removes existing subscription for given list of symbols. @@ -444,13 +489,20 @@ async def unsubscribe(self, event_type: EventType, symbols: list[str]) -> None: logger.debug('sending unsubscription: %s', message) await self._websocket.send(json.dumps([message])) - async def subscribe_candle(self, ticker: str, start_time: datetime, interval: str) -> None: + async def subscribe_candle( + self, + ticker: str, + start_time: datetime, + interval: str + ) -> None: """ Subscribes to candle-style 'OHLC' data for the given symbol. :param ticker: symbol to get date for :param start_time: starting time for the data range - :param interval: the width of each candle in time, e.g. '5m', '1h', '3d', '1w', '1mo' + :param interval: + the width of each candle in time, e.g. '5m', '1h', '3d', + '1w', '1mo' """ id = await self._next_id() message = { @@ -471,7 +523,7 @@ async def subscribe_candle(self, ticker: str, start_time: datetime, interval: st async def unsubscribe_candle(self, ticker: str, interval: str) -> None: """ - Removes existing :class:`~tastytrade.dxfeed.event.Candle` subscription for given list of symbols. + Removes existing candle subscription for given list of symbols. :param ticker: symbol to unsubscribe from :param interval: candle width to unsubscribe from @@ -490,7 +542,7 @@ async def unsubscribe_candle(self, ticker: str, interval: str) -> None: def _map_message(self, message) -> list[Event]: """ - Takes the raw JSON data and returns a list of parsed :class:`~tastytrade.dxfeed.event.Event` objects. + Takes the raw JSON data and returns a list of parsed event objects. """ # the first time around, types are shown if isinstance(message[0], str): @@ -518,4 +570,5 @@ def _map_message(self, message) -> list[Event]: elif msg_type == EventType.TRADE: return Trade.from_stream(data) else: - raise TastytradeError(f'Unknown message type received from streamer: {message}') + msg = f'Unknown message type received from streamer: {message}' + raise TastytradeError(msg) diff --git a/tastytrade/watchlists.py b/tastytrade/watchlists.py index 5c4edc3..88930ea 100644 --- a/tastytrade/watchlists.py +++ b/tastytrade/watchlists.py @@ -36,14 +36,21 @@ def get_pairs_watchlists(cls, session: Session) -> list['PairsWatchlist']: :return: a list of :class:`PairsWatchlist` objects. """ - response = requests.get(f'{session.base_url}/pairs-watchlists', headers=session.headers) + response = requests.get( + f'{session.base_url}/pairs-watchlists', + headers=session.headers + ) validate_response(response) data = response.json()['data']['items'] return [cls(**entry) for entry in data] @classmethod - def get_pairs_watchlist(cls, session: Session, name: str) -> 'PairsWatchlist': + def get_pairs_watchlist( + cls, + session: Session, + name: str + ) -> 'PairsWatchlist': """ Fetches a Tastytrade public pairs watchlist by name. @@ -52,7 +59,10 @@ def get_pairs_watchlist(cls, session: Session, name: str) -> 'PairsWatchlist': :return: a :class:`PairsWatchlist` object. """ - response = requests.get(f'{session.base_url}/pairs-watchlists/{name}', headers=session.headers) + response = requests.get( + f'{session.base_url}/pairs-watchlists/{name}', + headers=session.headers + ) validate_response(response) data = response.json()['data'] @@ -71,7 +81,11 @@ class Watchlist(TastytradeJsonDataclass): order_index: int = 9999 @classmethod - def get_public_watchlists(cls, session: Session, counts_only: bool = False) -> list['Watchlist']: + def get_public_watchlists( + cls, + session: Session, + counts_only: bool = False + ) -> list['Watchlist']: """ Fetches a list of all Tastytrade public watchlists. @@ -101,7 +115,10 @@ def get_public_watchlist(cls, session: Session, name: str) -> 'Watchlist': :return: a :class:`Watchlist` object. """ - response = requests.get(f'{session.base_url}/public-watchlists/{name}', headers=session.headers) + response = requests.get( + f'{session.base_url}/public-watchlists/{name}', + headers=session.headers + ) validate_response(response) data = response.json()['data'] @@ -117,7 +134,10 @@ def get_private_watchlists(cls, session: Session) -> list['Watchlist']: :return: a list of :class:`Watchlist` objects. """ - response = requests.get(f'{session.base_url}/watchlists', headers=session.headers) + response = requests.get( + f'{session.base_url}/watchlists', + headers=session.headers + ) validate_response(response) data = response.json()['data']['items'] @@ -134,7 +154,10 @@ def get_private_watchlist(cls, session: Session, name: str) -> 'Watchlist': :return: a :class:`Watchlist` object. """ - response = requests.get(f'{session.base_url}/watchlists/{name}', headers=session.headers) + response = requests.get( + f'{session.base_url}/watchlists/{name}', + headers=session.headers + ) validate_response(response) data = response.json()['data'] @@ -149,7 +172,10 @@ def remove_private_watchlist(cls, session: Session, name: str) -> None: :param session: the session to use for the request. :param name: the name of the watchlist to delete. """ - response = requests.delete(f'{session.base_url}/watchlists/{name}', headers=session.headers) + response = requests.delete( + f'{session.base_url}/watchlists/{name}', + headers=session.headers + ) validate_response(response) def upload_private_watchlist(self, session: Session) -> None: @@ -184,11 +210,21 @@ def add_symbol(self, symbol: str, instrument_type: InstrumentType) -> None: """ if self.watchlist_entries is None: self.watchlist_entries = [] - self.watchlist_entries.append({'symbol': symbol, 'instrument-type': instrument_type}) + self.watchlist_entries.append({ + 'symbol': symbol, + 'instrument-type': instrument_type + }) - def remove_symbol(self, symbol: str, instrument_type: InstrumentType) -> None: + def remove_symbol( + self, + symbol: str, + instrument_type: InstrumentType + ) -> None: """ Removes a symbol from the watchlist. """ if self.watchlist_entries is not None: - self.watchlist_entries.remove({'symbol': symbol, 'instrument-type': instrument_type}) + self.watchlist_entries.remove({ + 'symbol': symbol, + 'instrument-type': instrument_type + }) From 9a27ba4a6775262b1883dd275b30ec44c61c4f88 Mon Sep 17 00:00:00 2001 From: Graeme22 Date: Fri, 7 Jul 2023 22:27:13 -0500 Subject: [PATCH 3/3] fix #66 --- tastytrade/metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tastytrade/metrics.py b/tastytrade/metrics.py index 9fbef69..4510c2d 100644 --- a/tastytrade/metrics.py +++ b/tastytrade/metrics.py @@ -64,7 +64,6 @@ class MarketMetricInfo(TastytradeJsonDataclass): liquidity_value: Decimal liquidity_rank: Decimal liquidity_rating: int - created_at: datetime updated_at: datetime option_expiration_implied_volatilities: list[OptionExpirationImpliedVolatility] # noqa: E501 liquidity_running_state: Liquidity @@ -84,6 +83,7 @@ class MarketMetricInfo(TastytradeJsonDataclass): iv_hv_30_day_difference: Decimal price_earnings_ratio: Decimal earnings_per_share: Decimal + created_at: Optional[datetime] = None dividend_ex_date: Optional[date] = None dividend_next_date: Optional[date] = None dividend_pay_date: Optional[date] = None