From 2a4faa3aeb7837ee0f77891ef6dc3f1a2d0ae56f Mon Sep 17 00:00:00 2001 From: Graeme Holliday Date: Tue, 23 Jul 2024 17:04:58 -0500 Subject: [PATCH 1/4] prepare v8.1 release: use connection pooling, simplify code --- .github/CONTRIBUTING.md | 15 +- .github/workflows/python-app.yml | 4 +- Makefile | 24 +- docs/conf.py | 2 +- requirements.txt | 6 +- setup.py | 4 +- tastytrade/__init__.py | 72 +++++- tastytrade/account.py | 423 +++++++++---------------------- tastytrade/instruments.py | 368 +++++++-------------------- tastytrade/metrics.py | 69 ++--- tastytrade/order.py | 2 + tastytrade/search.py | 19 +- tastytrade/session.py | 252 +++++++----------- tastytrade/streamer.py | 19 +- tastytrade/utils.py | 2 +- tastytrade/watchlists.py | 120 ++------- tests/conftest.py | 4 +- tests/test_account.py | 4 +- tests/test_search.py | 14 + tests/test_session.py | 6 +- 20 files changed, 470 insertions(+), 959 deletions(-) create mode 100644 tests/test_search.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 482cc60..bf0f9ba 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,15 +1,10 @@ # Contributions -Since Tastytrade certification sessions are severely limited in capabilities, the test suite for this SDK requires the usage of your own Tastytrade credentials. In order to pass the tests, you'll need to set up your Tastytrade credentials as repository secrets on your local fork. - -Secrets are protected by Github and are not visible to anyone. You can read more about repository secrets [here](https://docs.github.com/en/actions/reference/encrypted-secrets). +Since Tastytrade certification sessions are severely limited in capabilities, the test suite for this SDK requires the usage of your own Tastytrade credentials. In order to pass the tests, you'll need to set use your Tastytrade credentials to run the tests on your local fork ## Steps to follow to contribute -1. Fork the repository to your personal Github account, NOT to an organization where others may be able to indirectly access your secrets. -2. Make your changes on the forked repository. -3. Go to the "Actions" page on the forked repository and enable actions. -4. Navigate to the forked repository's settings page and click on "Secrets and variables" > "Actions". -5. Click on "New repository secret" to add your Tastytrade username named `TT_USERNAME`. -6. Finally, do the same with your password, naming it `TT_PASSWORD`. -7. Make sure you have at least one share of long $F in your account, which will be used to place the OCO complex order (nothing will fill). +1. Fork the repository to your personal Github account and make your proposed changes. +2. Export your username, password, and account number to the following environment variables: `TT_USERNAME`, `TT_PASSWORD`, and `TT_ACCOUNT`. +3. Make sure you have at least one share of long $F in your account, which will be used to place the OCO complex order (nothing will fill). +4. Run `make venv` to create the virtual environment, then `make test` to run the tests locally. diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 77e4f1b..dd188fd 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -2,9 +2,9 @@ name: Python application on: push: - branches: [ master, advanced-streamer ] + branches: [ master ] pull_request: - branches: [ master, advanced-streamer ] + branches: [ master ] jobs: build: diff --git a/Makefile b/Makefile index 0f44586..46b05b0 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,19 @@ -.PHONY: clean venv test install docs - -clean: - find . -name '*.py[co]' -delete +.PHONY: venv lint test docs venv: - python -m venv env - env/bin/pip install -r requirements.txt + python -m venv .venv + .venv/bin/pip install -r requirements.txt + .venv/bin/pip install -e . + .venv/bin/pip install -r docs/requirements.txt lint: - isort --check --diff tastytrade/ tests/ - flake8 --count --show-source --statistics tastytrade/ tests/ - mypy -p tastytrade - mypy -p tests + .venv/bin/isort --check --diff tastytrade/ tests/ + .venv/bin/flake8 --count --show-source --statistics tastytrade/ tests/ + .venv/bin/mypy -p tastytrade + .venv/bin/mypy -p tests test: - python -m pytest --cov=tastytrade --cov-report=term-missing tests/ --cov-fail-under=95 - -install: - env/bin/pip install -e . + .venv/bin/pytest --cov=tastytrade --cov-report=term-missing tests/ --cov-fail-under=95 docs: cd docs; make html diff --git a/docs/conf.py b/docs/conf.py index eb73a8b..5bd1095 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'tastytrade' copyright = '2024, Graeme Holliday' author = 'Graeme Holliday' -release = '7.9' +release = '8.1' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/requirements.txt b/requirements.txt index 8d2da47..15be76e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ -requests==2.32.1 +httpx==0.27.0 mypy==1.10.0 flake8==7.0.0 isort==5.13.2 -types-requests==2.31.0.20240406 types-pytz==2024.1.0.20240417 websockets==12.0 pandas_market_calendars==4.3.3 @@ -10,4 +9,5 @@ pydantic==2.7.1 pytest==8.2.1 pytest_cov==5.0.0 pytest-asyncio==0.23.7 -fake-useragent==1.5.1 \ No newline at end of file +fake-useragent==1.5.1 +numpy==1.26.4 \ No newline at end of file diff --git a/setup.py b/setup.py index 26a04ed..5ac2b39 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='tastytrade', - version='7.9', + version='8.1', description='An unofficial SDK for Tastytrade!', long_description=LONG_DESCRIPTION, long_description_content_type='text/markdown', @@ -16,7 +16,7 @@ url='https://github.com/tastyware/tastytrade', license='MIT', install_requires=[ - 'requests<3', + 'httpx>=0.27.0', 'websockets>=11.0.3', 'pydantic>=2.6.3', 'pandas_market_calendars>=4.3.3', diff --git a/tastytrade/__init__.py b/tastytrade/__init__.py index 7d77520..13155f6 100644 --- a/tastytrade/__init__.py +++ b/tastytrade/__init__.py @@ -2,24 +2,78 @@ API_URL = 'https://api.tastyworks.com' CERT_URL = 'https://api.cert.tastyworks.com' -VERSION = '7.9' +VERSION = '8.1' logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -from .account import Account # noqa: E402 -from .search import symbol_search # noqa: E402 -from .session import CertificationSession, ProductionSession # noqa: E402 -from .streamer import AlertStreamer, DXLinkStreamer # noqa: E402 -from .watchlists import PairsWatchlist, Watchlist # noqa: E402 +# flake8: noqa + +from .account import Account +from .instruments import (Cryptocurrency, Equity, Future, FutureOption, + FutureOptionProduct, FutureProduct, + NestedFutureOptionChain, NestedOptionChain, Option, + OptionType, Warrant, get_future_option_chain, + get_option_chain, get_quantity_decimal_precisions) +from .metrics import (get_dividends, get_earnings, get_market_metrics, + get_risk_free_rate) +from .order import (ComplexOrderType, InstrumentType, NewComplexOrder, + NewOrder, OrderAction, OrderStatus, OrderTimeInForce, + OrderType, PriceEffect) +from .search import symbol_search +from .session import Session +from .streamer import AlertStreamer, AlertType, DXLinkStreamer +from .utils import (get_future_fx_monthly, get_future_grain_monthly, + get_future_index_monthly, get_future_metal_monthly, + get_future_oil_monthly, get_future_treasury_monthly, + get_tasty_monthly, get_third_friday, now_in_new_york, + today_in_new_york) +from .watchlists import PairsWatchlist, Watchlist __all__ = [ 'Account', 'AlertStreamer', - 'CertificationSession', + 'AlertType', + 'ComplexOrderType', + 'Cryptocurrency', 'DXLinkStreamer', + 'Equity', + 'Future', + 'FutureOption', + 'FutureOptionProduct', + 'FutureProduct', + 'InstrumentType', + 'NestedFutureOptionChain', + 'NestedOptionChain', + 'NewComplexOrder', + 'NewOrder', + 'Option', + 'OptionType', + 'OrderAction', + 'OrderStatus', + 'OrderTimeInForce', + 'OrderType', 'PairsWatchlist', - 'ProductionSession', + 'PriceEffect', + 'Session', + 'Warrant', 'Watchlist', - 'symbol_search' + 'get_dividends', + 'get_earnings', + 'get_future_fx_monthly', + 'get_future_grain_monthly', + 'get_future_index_monthly', + 'get_future_metal_monthly', + 'get_future_oil_monthly', + 'get_future_option_chain', + 'get_future_treasury_monthly', + 'get_market_metrics', + 'get_option_chain', + 'get_quantity_decimal_precisions', + 'get_risk_free_rate', + 'get_tasty_monthly', + 'get_third_friday', + 'now_in_new_york', + 'symbol_search', + 'today_in_new_york' ] diff --git a/tastytrade/account.py b/tastytrade/account.py index e79ae58..5d0bb90 100644 --- a/tastytrade/account.py +++ b/tastytrade/account.py @@ -2,13 +2,12 @@ from decimal import Decimal from typing import Any, Dict, List, Optional, Union -import requests from pydantic import BaseModel from tastytrade.order import (InstrumentType, NewComplexOrder, NewOrder, OrderAction, OrderStatus, PlacedComplexOrder, PlacedOrder, PlacedOrderResponse, PriceEffect) -from tastytrade.session import ProductionSession, Session +from tastytrade.session import Session from tastytrade.utils import (TastytradeError, TastytradeJsonDataclass, today_in_new_york, validate_response) @@ -153,6 +152,11 @@ class CurrentPosition(TastytradeJsonDataclass): realized_today_date: Optional[date] = None +class FeesInfo(TastytradeJsonDataclass): + total_fees: Decimal + total_fees_effect: PriceEffect + + class Lot(TastytradeJsonDataclass): """ Dataclass containing information about the lot of a position. @@ -405,66 +409,37 @@ def get_accounts( include_closed=False ) -> List['Account']: """ - Gets all trading accounts from the Tastyworks platform. By default - excludes closed accounts from the results. + Gets all trading accounts associated with the Tastytrade user. :param session: the session to use for the request. - - :return: a list of :class:`Account` objects. + :param include_closed: + whether to include closed accounts in the results (default False) """ - - response = requests.get( - f'{session.base_url}/customers/me/accounts', - headers=session.headers - ) - validate_response(response) # throws exception if not 200 - - accounts = [] - data = response.json()['data']['items'] - for entry in data: - account = entry['account'] - if not include_closed and account['is-closed']: - continue - accounts.append(cls(**account)) - - return accounts + data = session.get('/customers/me/accounts') + return [ + cls(**i['account']) + for i in data['items'] + if include_closed or not i['account']['is-closed'] + ] @classmethod def get_account(cls, session: Session, account_number: str) -> 'Account': """ - Returns a new :class:`Account` object for the given account ID. + Returns the Tastytrade account associated with the given account ID. :param session: the session to use for the request. :param account_number: the account ID to get. - - :return: :class:`Account` object corresponding to the given ID. """ - response = requests.get( - f'{session.base_url}/customers/me/accounts/{account_number}', - headers=session.headers - ) - validate_response(response) # throws exception if not 200 - - account = response.json()['data'] - return cls(**account) + data = session.get(f'/customers/me/accounts/{account_number}') + return cls(**data) def get_trading_status(self, session: Session) -> TradingStatus: """ Get the trading status of the account. :param session: the session to use for the request. - - :return: a Tastytrade 'TradingStatus' object in JSON format. """ - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}/' - 'trading-status'), - headers=session.headers - ) - validate_response(response) # throws exception if not 200 - - data = response.json()['data'] - + data = session.get(f'/accounts/{self.account_number}/trading-status') return TradingStatus(**data) def get_balances(self, session: Session) -> AccountBalance: @@ -472,17 +447,8 @@ def get_balances(self, session: Session) -> AccountBalance: Get the current balances of the account. :param session: the session to use for the request. - - :return: a Tastytrade 'AccountBalance' object in JSON format. """ - response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/balances', - headers=session.headers - ) - validate_response(response) # throws exception if not 200 - - data = response.json()['data'] - + data = session.get(f'/accounts/{self.account_number}/balances') return AccountBalance(**data) def get_balance_snapshots( @@ -503,26 +469,16 @@ def get_balance_snapshots( :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'. - - :return: - a list of two Tastytrade 'AccountBalanceSnapshot' in JSON format. """ - params: Dict[str, Any] = { + params = { 'snapshot-date': snapshot_date, 'time-of-day': time_of_day } - - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}/balance-' - 'snapshots'), - headers=session.headers, + data = session.get( + f'/accounts/{self.account_number}/balance-snapshots', params={k: v for k, v in params.items() if v is not None} ) - validate_response(response) # throws exception if not 200 - - data = response.json()['data']['items'] - - return [AccountBalanceSnapshot(**entry) for entry in data] + return [AccountBalanceSnapshot(**i) for i in data['items']] def get_positions( self, @@ -530,11 +486,11 @@ def get_positions( underlying_symbols: Optional[List[str]] = None, symbol: Optional[str] = None, instrument_type: Optional[InstrumentType] = None, - include_closed: bool = False, + include_closed: Optional[bool] = None, underlying_product_code: Optional[str] = None, partition_keys: Optional[List[str]] = None, - net_positions: bool = False, - include_marks: bool = False + net_positions: Optional[bool] = None, + include_marks: Optional[bool] = None ) -> List[CurrentPosition]: """ Get the current positions of the account. @@ -552,10 +508,8 @@ def get_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. """ - params: Dict[str, Any] = { + params = { 'underlying-symbol[]': underlying_symbols, 'symbol': symbol, 'instrument-type': instrument_type, @@ -565,16 +519,11 @@ def get_positions( 'net-positions': net_positions, 'include-marks': include_marks } - response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/positions', - headers=session.headers, + data = session.get( + f'/accounts/{self.account_number}/positions', params={k: v for k, v in params.items() if v is not None} ) - validate_response(response) # throws exception if not 200 - - data = response.json()['data']['items'] - - return [CurrentPosition(**entry) for entry in data] + return [CurrentPosition(**i) for i in data['items']] def get_history( self, @@ -586,7 +535,7 @@ def get_history( types: Optional[List[str]] = None, sub_types: Optional[List[str]] = None, start_date: Optional[date] = None, - end_date: date = today_in_new_york(), + end_date: Optional[date] = None, instrument_type: Optional[InstrumentType] = None, symbol: Optional[str] = None, underlying_symbol: Optional[str] = None, @@ -621,8 +570,6 @@ def get_history( 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. """ # if a specific page is provided, we just get that page; # otherwise, we loop through all pages @@ -630,7 +577,7 @@ def get_history( if page_offset is None: page_offset = 0 paginate = True - params: Dict[str, Any] = { + params = { 'per-page': per_page, 'page-offset': page_offset, 'sort': sort, @@ -648,81 +595,62 @@ def get_history( 'start-at': start_at, 'end-at': end_at } - # loop through pages and get all transactions - results = [] + txns = [] while True: - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}/' - 'transactions'), - headers=session.headers, - params={k: v for k, v in params.items() if v is not None} + response = session.client.get( + f'/accounts/{self.account_number}/transactions', + params={ + k: v # type: ignore + for k, v in params.items() + if v is not None + } ) validate_response(response) - json = response.json() - results.extend(json['data']['items']) - + txns.extend([Transaction(**i) for i in json['data']['items']]) + # handle pagination pagination = json['pagination'] - if pagination['page-offset'] >= pagination['total-pages'] - 1: - break - if not paginate: + if ( + pagination['page-offset'] >= pagination['total-pages'] - 1 or + not paginate + ): break params['page-offset'] += 1 # type: ignore - return [Transaction(**entry) for entry in results] + return txns - def get_transaction( - self, - session: Session, - id: int - ) -> Transaction: # pragma: no cover + def get_transaction(self, session: Session, id: int) -> Transaction: """ Get a single transaction by ID. :param session: the session to use for the request. :param id: the ID of the transaction to fetch. - - :return: a Tastytrade 'Transaction' object in JSON format. """ - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}/transactions' - f'/{id}'), - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/accounts/{self.account_number}/transactions/' + f'{id}') return Transaction(**data) def get_total_fees( self, session: Session, date: date = today_in_new_york() - ) -> Dict[str, Any]: + ) -> FeesInfo: """ Get the total fees for a given date. :param session: the session to use for the request. :param date: the date to get fees for. - - :return: a dict containing the total fees and the price effect. """ - params: Dict[str, Any] = {'date': date} - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}/transactions/' - 'total-fees'), - headers=session.headers, - params=params + data = session.get( + f'/accounts/{self.account_number}/transactions/total-fees', + params={'date': date} ) - validate_response(response) - - return response.json()['data'] + return FeesInfo(**data) def get_net_liquidating_value_history( self, - session: ProductionSession, + session: Session, time_back: Optional[str] = None, start_time: Optional[datetime] = None ) -> List[NetLiqOhlc]: @@ -739,10 +667,8 @@ def get_net_liquidating_value_history( :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. - - :return: a list of Tastytrade 'NetLiqOhlc' objects in JSON format. """ - params: Dict[str, Any] = {} + params = {} if start_time: # format to Tastytrade DateTime format params = {'start-time': start_time.strftime('%Y-%m-%dT%H:%M:%SZ')} @@ -751,41 +677,24 @@ def get_net_liquidating_value_history( raise TastytradeError(msg) else: params = {'time-back': time_back} - - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}/net-liq/' - 'history'), - headers=session.headers, + data = session.get( + f'/accounts/{self.account_number}/net-liq/history', params=params ) - validate_response(response) - - data = response.json()['data']['items'] - - return [NetLiqOhlc(**entry) for entry in data] + return [NetLiqOhlc(**i) for i in data['items']] def get_position_limit(self, session: Session) -> PositionLimit: """ Get the maximum order size information for the account. :param session: the session to use for the request. - - :return: a Tastytrade 'PositionLimit' object in JSON format. """ - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}/position-' - 'limit'), - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/accounts/{self.account_number}/position-limit') return PositionLimit(**data) def get_effective_margin_requirements( self, - session: ProductionSession, + session: Session, symbol: str ) -> MarginRequirement: """ @@ -794,20 +703,11 @@ def get_effective_margin_requirements( :param session: the session to use for the request, can't be certification :param symbol: the symbol to get margin requirements for. - - :return: a :class:`MarginRequirement` object. """ if symbol: symbol = symbol.replace('/', '%2F') - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}/margin-' - f'requirements/{symbol}/effective'), - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/accounts/{self.account_number}/margin-' + f'requirements/{symbol}/effective') return MarginRequirement(**data) def get_margin_requirements(self, session: Session) -> MarginReport: @@ -816,18 +716,9 @@ def get_margin_requirements(self, session: Session) -> MarginReport: 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'), - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/margin/accounts/{self.account_number}' + f'/requirements') return MarginReport(**data) def get_live_orders(self, session: Session) -> List[PlacedOrder]: @@ -835,18 +726,9 @@ def get_live_orders(self, session: Session) -> List[PlacedOrder]: Get orders placed today for the account. :param session: the session to use for the request. - - :return: a list of :class:`PlacedOrder` objects. """ - response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/orders/live', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['items'] - - return [PlacedOrder(**entry) for entry in data] + data = session.get(f'/accounts/{self.account_number}/orders/live') + return [PlacedOrder(**i) for i in data['items']] def get_live_complex_orders( self, @@ -856,19 +738,10 @@ def get_live_complex_orders( Get complex orders placed today for the account. :param session: the session to use for the request. - - :return: a list of :class:`PlacedComplexOrder` objects. """ - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}' - f'/complex-orders/live'), - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['items'] - - return [PlacedComplexOrder(**entry) for entry in data] + data = session.get(f'/accounts/{self.account_number}/complex-' + f'orders/live') + return [PlacedComplexOrder(**i) for i in data['items']] def get_complex_order( self, @@ -879,19 +752,10 @@ def get_complex_order( Gets a complex order with the given ID. :param session: the session to use for the request. - - :return: - a :class:`PlacedComplexOrder` object corresponding to the given ID + :param order_id: the ID of the order to fetch. """ - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}/complex-' - f'orders/{order_id}'), - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/accounts/{self.account_number}/complex-' + f'orders/{order_id}') return PlacedComplexOrder(**data) def get_order(self, session: Session, order_id: int) -> PlacedOrder: @@ -899,18 +763,10 @@ def get_order(self, session: Session, order_id: int) -> PlacedOrder: Gets an order with the given ID. :param session: the session to use for the request. - - :return: a :class:`PlacedOrder` object corresponding to the given ID + :param order_id: the ID of the order to fetch. """ - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}/orders' - f'/{order_id}'), - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/accounts/{self.account_number}/orders' + f'/{order_id}') return PlacedOrder(**data) def delete_complex_order(self, session: Session, order_id: int) -> None: @@ -920,12 +776,8 @@ def delete_complex_order(self, session: Session, order_id: int) -> None: :param session: the session to use for the request. :param order_id: the ID of the order to delete. """ - response = requests.delete( - (f'{session.base_url}/accounts/{self.account_number}/complex-' - f'orders/{order_id}'), - headers=session.headers - ) - validate_response(response) + session.delete(f'/accounts/{self.account_number}/complex-' + f'orders/{order_id}') def delete_order(self, session: Session, order_id: int) -> None: """ @@ -934,12 +786,7 @@ def delete_order(self, session: Session, order_id: int) -> None: :param session: the session to use for the request. :param order_id: the ID of the order to delete. """ - response = requests.delete( - (f'{session.base_url}/accounts/{self.account_number}/orders' - f'/{order_id}'), - headers=session.headers - ) - validate_response(response) + session.delete(f'/accounts/{self.account_number}/orders/{order_id}') def get_order_history( self, @@ -952,7 +799,7 @@ def get_order_history( statuses: Optional[List[OrderStatus]] = None, futures_symbol: Optional[str] = None, underlying_instrument_type: Optional[InstrumentType] = None, - sort: str = 'Desc', + sort: Optional[str] = None, start_at: Optional[datetime] = None, end_at: Optional[datetime] = None ) -> List[PlacedOrder]: @@ -975,8 +822,6 @@ def get_order_history( 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. """ # if a specific page is provided, we just get that page; # otherwise, we loop through all pages @@ -984,7 +829,7 @@ def get_order_history( if page_offset is None: page_offset = 0 paginate = True - params: Dict[str, Any] = { + params = { 'per-page': per_page, 'page-offset': page_offset, 'start-date': start_date, @@ -997,28 +842,30 @@ def get_order_history( 'start-at': start_at, 'end-at': end_at } - # loop through pages and get all transactions - results = [] + orders = [] while True: - response = requests.get( - f'{session.base_url}/accounts/{self.account_number}/orders', - headers=session.headers, - params={k: v for k, v in params.items() if v is not None} + response = session.client.get( + f'/accounts/{self.account_number}/orders', + params={ + k: v # type: ignore + for k, v in params.items() + if v is not None + } ) validate_response(response) - json = response.json() - results.extend(json['data']['items']) - + orders.extend([PlacedOrder(**i) for i in json['data']['items']]) + # handle pagination pagination = json['pagination'] - if pagination['page-offset'] >= pagination['total-pages'] - 1: - break - if not paginate: + if ( + pagination['page-offset'] >= pagination['total-pages'] - 1 or + not paginate + ): break params['page-offset'] += 1 # type: ignore - return [PlacedOrder(**entry) for entry in results] + return orders def get_complex_order_history( self, @@ -1033,9 +880,6 @@ def get_complex_order_history( :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 - - :return: - a list of Tastytrade 'PlacedComplexOrder' objects in JSON format. """ # if a specific page is provided, we just get that page; # otherwise, we loop through all pages @@ -1043,40 +887,38 @@ def get_complex_order_history( if page_offset is None: page_offset = 0 paginate = True - params: Dict[str, Any] = { + params = { 'per-page': per_page, 'page-offset': page_offset } - # loop through pages and get all transactions - results = [] + orders = [] while True: - response = requests.get( - (f'{session.base_url}/accounts/{self.account_number}' - f'/complex-orders'), - headers=session.headers, + response = session.client.get( + f'/accounts/{self.account_number}/complex-orders', params={k: v for k, v in params.items() if v is not None} ) validate_response(response) - json = response.json() - results.extend(json['data']['items']) - + orders.extend( + [PlacedComplexOrder(**i) for i in json['data']['items']] + ) + # handle pagination pagination = json['pagination'] - if pagination['page-offset'] >= pagination['total-pages'] - 1: - break - if not paginate: + if ( + pagination['page-offset'] >= pagination['total-pages'] - 1 or + not paginate + ): break params['page-offset'] += 1 # type: ignore - return [PlacedComplexOrder(**entry) for entry in results] + return orders def place_order( self, session: Session, order: NewOrder, - dry_run: bool = True, - raise_errors: bool = True + dry_run: bool = True ) -> PlacedOrderResponse: """ Place the given order. @@ -1084,25 +926,12 @@ def place_order( :param session: the session to use for the request. :param order: the order to place. :param dry_run: whether this is a test order or not. - :param raise_errors: - whether to raise errors. we may just want to see the BP usage for - an order even if it couldn't be placed. Note in some circumstances - an error may still be raised if the response is invalid. - - :return: a :class:`PlacedOrderResponse` object for the placed order. """ - url = f'{session.base_url}/accounts/{self.account_number}/orders' + url = f'/accounts/{self.account_number}/orders' if dry_run: url += '/dry-run' json = order.model_dump_json(exclude_none=True, by_alias=True) - - response = requests.post(url, headers=session.headers, data=json) - # sometimes we just want to see BP usage for an invalid trade - if raise_errors: - validate_response(response) - - data = response.json()['data'] - + data = session.post(url, data=json) return PlacedOrderResponse(**data) def place_complex_order( @@ -1117,20 +946,12 @@ def place_complex_order( :param session: the session to use for the request. :param order: the order to place. :param dry_run: whether this is a test order or not. - - :return: a :class:`PlacedOrderResponse` object for the placed order. """ - url = (f'{session.base_url}/accounts/{self.account_number}' - '/complex-orders') + url = f'/accounts/{self.account_number}/complex-orders' if dry_run: url += '/dry-run' json = order.model_dump_json(exclude_none=True, by_alias=True) - - response = requests.post(url, headers=session.headers, data=json) - validate_response(response) - - data = response.json()['data'] - + data = session.post(url, data=json) return PlacedOrderResponse(**data) def replace_order( @@ -1146,21 +967,13 @@ def replace_order( :param session: the session to use for the request. :param old_order_id: the ID of the order to replace. :param new_order: the new order to replace the old order with. - - :return: a :class:`PlacedOrder` object for the modified order. """ - response = requests.put( - (f'{session.base_url}/accounts/{self.account_number}/orders' - f'/{old_order_id}'), - headers=session.headers, + data = session.put( + f'/accounts/{self.account_number}/orders/{old_order_id}', data=new_order.model_dump_json( exclude={'legs'}, exclude_none=True, by_alias=True ) ) - validate_response(response) - - data = response.json()['data'] - return PlacedOrder(**data) diff --git a/tastytrade/instruments.py b/tastytrade/instruments.py index 02e9a0e..c4c8111 100644 --- a/tastytrade/instruments.py +++ b/tastytrade/instruments.py @@ -1,13 +1,12 @@ import re +from collections import defaultdict from datetime import date, datetime from decimal import Decimal from enum import Enum -from typing import Any, Dict, List, Optional - -import requests +from typing import Dict, List, Optional from tastytrade.order import InstrumentType, TradeableTastytradeJsonDataclass -from tastytrade.session import ProductionSession, Session +from tastytrade.session import Session from tastytrade.utils import TastytradeJsonDataclass, validate_response @@ -186,27 +185,19 @@ class Cryptocurrency(TradeableTastytradeJsonDataclass): @classmethod def get_cryptocurrencies( - cls, session: Session, symbols: List[str] = [] + cls, + session: Session, + symbols: List[str] = [] ) -> List['Cryptocurrency']: """ 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 cryptocurrency objects. """ params = {'symbol[]': symbols} if symbols else None - response = requests.get( - f'{session.base_url}/instruments/cryptocurrencies', - headers=session.headers, - params=params - ) - validate_response(response) - - data = response.json()['data']['items'] - - return [cls(**entry) for entry in data] + data = session.get('/instruments/cryptocurrencies', params=params) + return [cls(**i) for i in data['items']] @classmethod def get_cryptocurrency( @@ -215,22 +206,13 @@ def get_cryptocurrency( symbol: str ) -> 'Cryptocurrency': """ - Returns a :class:`Cryptocurrency` object from the given symbol. + Returns a Cryptocurrency object from the given symbol. :param session: the session to use for the request. :param symbol: the symbol to get the cryptocurrency for. - - :return: a :class:`Cryptocurrency` object. """ symbol = symbol.replace('/', '%2F') - response = requests.get( - f'{session.base_url}/instruments/cryptocurrencies/{symbol}', - headers=session.headers, - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/instruments/cryptocurrencies/{symbol}') return cls(**data) @@ -269,7 +251,7 @@ def get_active_equities( lendability: Optional[str] = None ) -> List['Equity']: """ - Returns a list of actively traded :class:`Equity` objects. + Returns a list of actively traded Equity objects. :param session: the session to use for the request. :param per_page: the number of equities to get per page. @@ -278,8 +260,6 @@ def get_active_equities( :param lendability: the lendability of the equities; e.g. 'Easy To Borrow', 'Locate Required', 'Preborrow' - - :return: a list of :class:`Equity` objects. """ # if a specific page is provided, we just get that page; # otherwise, we loop through all pages @@ -287,30 +267,27 @@ def get_active_equities( if page_offset is None: page_offset = 0 paginate = True - params: Dict[str, Any] = { + params = { 'per-page': per_page, 'page-offset': page_offset, 'lendability': lendability } - # loop through pages and get all active equities equities = [] while True: - response = requests.get( - f'{session.base_url}/instruments/equities/active', - headers=session.headers, - params=params + response = session.client.get( + '/instruments/equities/active', + params={k: v for k, v in params.items() if v is not None} ) validate_response(response) - json = response.json() - data = json['data']['items'] - equities.extend([cls(**entry) for entry in data]) - + equities.extend([cls(**i) for i in json['data']['items']]) + # handle pagination pagination = json['pagination'] - if pagination['page-offset'] >= pagination['total-pages'] - 1: - break - if not paginate: + if ( + pagination['page-offset'] >= pagination['total-pages'] - 1 or + not paginate + ): break params['page-offset'] += 1 # type: ignore @@ -326,7 +303,7 @@ def get_equities( is_etf: Optional[bool] = None ) -> List['Equity']: """ - Returns a list of :class:`Equity` objects from the given symbols. + Returns a list of Equity objects from the given symbols. :param session: the session to use for the request. :param symbols: the symbols to get the equities for. @@ -335,45 +312,29 @@ def get_equities( 'Locate Required', 'Preborrow' :param is_index: whether the equities are indexes. :param is_etf: whether the equities are ETFs. - - :return: a list of :class:`Equity` objects. """ - params: Dict[str, Any] = { + params = { 'symbol[]': symbols, 'lendability': lendability, 'is-index': is_index, 'is-etf': is_etf } - response = requests.get( - f'{session.base_url}/instruments/equities', - headers=session.headers, + data = session.get( + '/instruments/equities', params={k: v for k, v in params.items() if v is not None} ) - validate_response(response) - - data = response.json()['data']['items'] - - return [cls(**entry) for entry in data] + return [cls(**i) for i in data['items']] @classmethod def get_equity(cls, session: Session, symbol: str) -> 'Equity': """ - Returns a :class:`Equity` object from the given symbol. + Returns a Equity object from the given symbol. :param session: the session to use for the request. :param symbol: the symbol to get the equity for. - - :return: a :class:`Equity` object. """ symbol = symbol.replace('/', '%2F') - response = requests.get( - f'{session.base_url}/instruments/equities/{symbol}', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/instruments/equities/{symbol}') return cls(**data) @@ -415,32 +376,25 @@ def get_options( symbols: Optional[List[str]] = None, active: Optional[bool] = None, with_expired: Optional[bool] = None - ) -> List['Option']: # pragma: no cover + ) -> List['Option']: """ - Returns a list of :class:`Option` objects from the given symbols. + Returns a list of Option objects from the given symbols. :param session: the session to use for the request. :param symbols: the OCC symbols to get the options for. :param active: whether the options are active. :param with_expired: whether to include expired options. - - :return: a list of :class:`Option` objects. """ - params: Dict[str, Any] = { + params = { 'symbol[]': symbols, 'active': active, 'with-expired': with_expired } - response = requests.get( - f'{session.base_url}/instruments/equity-options', - headers=session.headers, + data = session.get( + '/instruments/equity-options', params={k: v for k, v in params.items() if v is not None} ) - validate_response(response) - - data = response.json()['data']['items'] - - return [cls(**entry) for entry in data] + return [cls(**i) for i in data['items']] @classmethod def get_option( @@ -450,24 +404,17 @@ def get_option( active: Optional[bool] = None ) -> 'Option': """ - Returns a :class:`Option` object from the given symbol. + Returns a Option object from the given symbol. :param session: the session to use for the request. :param symbol: the symbol to get the option for, OCC format - - :return: a :class:`Option` object. """ symbol = symbol.replace('/', '%2F') params = {'active': active} if active is not None else None - response = requests.get( - f'{session.base_url}/instruments/equity-options/{symbol}', - headers=session.headers, + data = session.get( + f'/instruments/equity-options/{symbol}', params=params ) - validate_response(response) - - data = response.json()['data'] - return cls(**data) def _set_streamer_symbol(self) -> None: @@ -488,8 +435,6 @@ def streamer_symbol_to_occ(cls, streamer_symbol) -> str: Returns the OCC 2010 symbol equivalent to the given streamer symbol. :param streamer_symbol: the streamer symbol to convert - - :return: the equivalent OCC 2010 symbol """ match = re.match( r'\.([A-Z]+)(\d{6})([CP])(\d+)(\.(\d+))?', @@ -515,8 +460,6 @@ def occ_to_streamer_symbol(cls, occ) -> str: 2010 symbol. :param occ: the OCC symbol to convert - - :return: the equivalent streamer symbol """ match = re.match( r'([A-Z]+)\s+(\d{6})([CP])(\d{5})(\d{3})', @@ -562,19 +505,10 @@ def get_chain(cls, session: Session, symbol: str) -> 'NestedOptionChain': :param session: the session to use for the request. :param symbol: the symbol to get the option chain for. - - :return: a :class:`NestedOptionChain` object. """ symbol = symbol.replace('/', '%2F') - response = requests.get( - f'{session.base_url}/option-chains/{symbol}/nested', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['items'][0] - - return cls(**data) + data = session.get(f'/option-chains/{symbol}/nested') + return cls(**data['items'][0]) class FutureProduct(TastytradeJsonDataclass): @@ -622,21 +556,12 @@ def get_future_products( session: Session ) -> List['FutureProduct']: """ - Returns a list of :class:`FutureProduct` objects available. + Returns a list of FutureProduct objects available. :param session: the session to use for the request. - - :return: a list of :class:`FutureProduct` objects. """ - response = requests.get( - f'{session.base_url}/instruments/future-products', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['items'] - - return [cls(**entry) for entry in data] + data = session.get('/instruments/future-products') + return [cls(**i) for i in data['items']] @classmethod def get_future_product( @@ -646,25 +571,15 @@ def get_future_product( exchange: str = 'CME' ) -> 'FutureProduct': """ - Returns a :class:`FutureProduct` object from the given symbol. + Returns a FutureProduct object from the given symbol. :param session: the session to use for the request. :param code: the product code, e.g. 'ES' :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}/' - f'{code}'), - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/instruments/future-products/{exchange}/{code}') return cls(**data) @@ -713,7 +628,7 @@ def get_futures( product_codes: Optional[List[str]] = None ) -> List['Future']: """ - Returns a list of :class:`Future` objects from the given symbols + Returns a list of Future objects from the given symbols or product codes. :param session: the session to use for the request. @@ -722,43 +637,27 @@ def get_futures( :param product_codes: the product codes of the futures, e.g. 'ES', '6A'. Ignored if symbols are provided. - - :return: a list of :class:`Future` objects. """ - params: Dict[str, Any] = { + params = { 'symbol[]': symbols, 'product-code[]': product_codes } - response = requests.get( - f'{session.base_url}/instruments/futures', - headers=session.headers, + data = session.get( + '/instruments/futures', params={k: v for k, v in params.items() if v is not None} ) - validate_response(response) - - data = response.json()['data']['items'] - - return [cls(**entry) for entry in data] + return [cls(**i) for i in data['items']] @classmethod def get_future(cls, session: Session, symbol: str) -> 'Future': """ - Returns a :class:`Future` object from the given symbol. + Returns a Future object from the given symbol. :param session: the session to use for the request. :param symbol: the symbol to get the future for. - - :return: a :class:`Future` object. """ symbol = symbol.replace('/', '') - response = requests.get( - f'{session.base_url}/instruments/futures/{symbol}', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/instruments/futures/{symbol}') return cls(**data) @@ -792,21 +691,12 @@ def get_future_option_products( session: Session ) -> List['FutureOptionProduct']: """ - Returns a list of :class:`FutureOptionProduct` objects available. + Returns a list of FutureOptionProduct objects available. :param session: the session to use for the request. - - :return: a list of :class:`FutureOptionProduct` objects. """ - response = requests.get( - f'{session.base_url}/instruments/future-option-products', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['items'] - - return [cls(**entry) for entry in data] + data = session.get('/instruments/future-option-products') + return [cls(**i) for i in data['items']] @classmethod def get_future_option_product( @@ -816,24 +706,15 @@ def get_future_option_product( exchange: str = 'CME' ) -> 'FutureOptionProduct': """ - Returns a :class:`FutureOptionProduct` object from the given symbol. + Returns a FutureOptionProduct object from the given symbol. :param session: the session to use for the request. :param code: the root symbol of the future option :param exchange: the exchange to get the product from - - :return: a :class:`FutureOptionProduct` object. """ root_symbol = root_symbol.replace('/', '') - response = requests.get( - (f'{session.base_url}/instruments/future-option-products/' - f'{exchange}/{root_symbol}'), - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/instruments/future-option-products/' + f'{exchange}/{root_symbol}') return cls(**data) @@ -887,9 +768,9 @@ def get_future_options( strike_price: Optional[Decimal] = None ) -> List['FutureOption']: """ - Returns a list of :class:`FutureOption` objects from the given symbols. + Returns a list of FutureOption objects from the given symbols. - NOTE: As far as I can tell, all of the parameters are bugged except + NOTE: Last I checked, all of the parameters are bugged except for `symbols`. :param session: the session to use for the request. @@ -899,26 +780,19 @@ def get_future_options( :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. - - :return: a list of :class:`FutureOption` objects. """ - params: Dict[str, Any] = { + params = { 'symbol[]': symbols, 'option-root-symbol': root_symbol, 'expiration-date': expiration_date, 'option-type': option_type, 'strike-price': strike_price } - response = requests.get( - f'{session.base_url}/instruments/future-options', - headers=session.headers, + data = session.get( + '/instruments/future-options', params={k: v for k, v in params.items() if v is not None} ) - validate_response(response) - - data = response.json()['data']['items'] - - return [cls(**entry) for entry in data] + return [cls(**i) for i in data['items']] @classmethod def get_future_option( @@ -927,22 +801,13 @@ def get_future_option( symbol: str ) -> 'FutureOption': """ - Returns a :class:`FutureOption` object from the given symbol. + Returns a FutureOption object from the given symbol. :param session: the session to use for the request. :param symbol: the symbol to get the option for, Tastytrade format - - :return: a :class:`FutureOption` object. """ symbol = symbol.replace('/', '%2F').replace(' ', '%20') - response = requests.get( - f'{session.base_url}/instruments/future-options/{symbol}', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/instruments/future-options/{symbol}') return cls(**data) @@ -980,18 +845,9 @@ def get_chain( :param session: the session to use for the request. :param symbol: the symbol to get the option chain for. - - :return: a :class:`NestedFutureOptionChain` object. """ symbol = symbol.replace('/', '') - response = requests.get( - f'{session.base_url}/futures-option-chains/{symbol}/nested', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/futures-option-chains/{symbol}/nested') return cls(**data) @@ -1015,43 +871,24 @@ def get_warrants( symbols: Optional[List[str]] = None ) -> List['Warrant']: """ - Returns a list of :class:`Warrant` objects from the given symbols. + Returns a list of Warrant objects from the given symbols. :param session: the session to use for the request. :param symbols: symbols of the warrants, e.g. 'NKLAW' - - :return: a list of :class:`Warrant` objects. """ - params = {'symbol[]': symbols} if symbols is not None else {} - response = requests.get( - f'{session.base_url}/instruments/warrants', - headers=session.headers, - params=params - ) - validate_response(response) - - data = response.json()['data']['items'] - - return [cls(**entry) for entry in data] + params = {'symbol[]': symbols} if symbols else None + data = session.get('/instruments/warrants', params=params) + return [cls(**i) for i in data['items']] @classmethod def get_warrant(cls, session: Session, symbol: str) -> 'Warrant': """ - Returns a :class:`Warrant` object from the given symbol. + Returns a Warrant object from the given symbol. :param session: the session to use for the request. :param symbol: the symbol to get the warrant for. - - :return: a :class:`Warrant` object. """ - response = requests.get( - f'{session.base_url}/instruments/warrants/{symbol}', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/instruments/warrants/{symbol}') return cls(**data) @@ -1063,22 +900,13 @@ def get_quantity_decimal_precisions( session: Session ) -> List[QuantityDecimalPrecision]: """ - Returns a list of :class:`QuantityDecimalPrecision` objects for different + Returns a list of QuantityDecimalPrecision objects for different types of instruments. :param session: the session to use for the request. - - :return: a list of :class:`QuantityDecimalPrecision` objects. """ - response = requests.get( - f'{session.base_url}/instruments/quantity-decimal-precisions', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['items'] - - return [QuantityDecimalPrecision(**entry) for entry in data] + data = session.get('/instruments/quantity-decimal-precisions') + return [QuantityDecimalPrecision(**i) for i in data['items']] def get_option_chain( @@ -1096,30 +924,19 @@ def get_option_chain( :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 options """ symbol = symbol.replace('/', '%2F') - response = requests.get( - f'{session.base_url}/option-chains/{symbol}', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['items'] - chain = {} - for entry in data: - option = Option(**entry) - if option.expiration_date not in chain: - chain[option.expiration_date] = [option] - else: - chain[option.expiration_date].append(option) + data = session.get(f'/option-chains/{symbol}') + chain = defaultdict(list) + for i in data['items']: + option = Option(**i) + chain[option.expiration_date].append(option) return chain def get_future_option_chain( - session: ProductionSession, + session: Session, symbol: str ) -> Dict[date, List[FutureOption]]: """ @@ -1129,27 +946,16 @@ def get_future_option_chain( 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. + 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 futures options. """ symbol = symbol.replace('/', '') - response = requests.get( - f'{session.base_url}/futures-option-chains/{symbol}', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['items'] - chain = {} - for entry in data: - option = FutureOption(**entry) - if option.expiration_date not in chain: - chain[option.expiration_date] = [option] - else: - chain[option.expiration_date].append(option) + data = session.get(f'/futures-option-chains/{symbol}') + chain = defaultdict(list) + for i in data['items']: + option = FutureOption(**i) + chain[option.expiration_date].append(option) return chain diff --git a/tastytrade/metrics.py b/tastytrade/metrics.py index e378c3b..0d0821e 100644 --- a/tastytrade/metrics.py +++ b/tastytrade/metrics.py @@ -1,11 +1,9 @@ from datetime import date, datetime from decimal import Decimal -from typing import Any, Dict, List, Optional +from typing import List, Optional -import requests - -from tastytrade.session import ProductionSession, Session -from tastytrade.utils import TastytradeJsonDataclass, validate_response +from tastytrade.session import Session +from tastytrade.utils import TastytradeJsonDataclass class DividendInfo(TastytradeJsonDataclass): @@ -108,7 +106,7 @@ class MarketMetricInfo(TastytradeJsonDataclass): def get_market_metrics( - session: ProductionSession, + session: Session, symbols: List[str] ) -> List[MarketMetricInfo]: """ @@ -116,23 +114,16 @@ def get_market_metrics( :param session: active user session to use :param symbols: list of symbols to retrieve metrics for - - :return: a list of Tastytrade 'MarketMetricInfo' objects in JSON format. """ - response = requests.get( - f'{session.base_url}/market-metrics', - headers=session.headers, + data = session.get( + '/market-metrics', params={'symbols': ','.join(symbols)} ) - validate_response(response) - - data = response.json()['data']['items'] - - return [MarketMetricInfo(**entry) for entry in data] + return [MarketMetricInfo(**i) for i in data['items']] def get_dividends( - session: ProductionSession, + session: Session, symbol: str ) -> List[DividendInfo]: """ @@ -140,24 +131,15 @@ def get_dividends( :param session: active user session to use :param symbol: symbol to retrieve dividend information for - - :return: a list of Tastytrade 'DividendInfo' objects in JSON format. """ symbol = symbol.replace('/', '%2F') - response = requests.get( - (f'{session.base_url}/market-metrics/historic-corporate-events/' - f'dividends/{symbol}'), - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['items'] - - return [DividendInfo(**entry) for entry in data] + data = session.get(f'/market-metrics/historic-corporate-events/' + f'dividends/{symbol}') + return [DividendInfo(**i) for i in data['items']] def get_earnings( - session: ProductionSession, + session: Session, symbol: str, start_date: date ) -> List[EarningsInfo]: @@ -167,22 +149,15 @@ def get_earnings( :param session: active user session to use :param symbol: symbol to retrieve earnings information for :param start_date: limits earnings to those on or after the given date - - :return: a list of Tastytrade 'EarningsInfo' objects in JSON format. """ symbol = symbol.replace('/', '%2F') - params: Dict[str, Any] = {'start-date': start_date} - response = requests.get( - (f'{session.base_url}/market-metrics/historic-corporate-events/' + params = {'start-date': start_date} + data = session.get( + (f'/market-metrics/historic-corporate-events/' f'earnings-reports/{symbol}'), - headers=session.headers, params=params ) - validate_response(response) - - data = response.json()['data']['items'] - - return [EarningsInfo(**entry) for entry in data] + return [EarningsInfo(**i) for i in data['items']] def get_risk_free_rate(session: Session) -> Decimal: @@ -190,14 +165,6 @@ def get_risk_free_rate(session: Session) -> Decimal: Retrieves the current risk-free rate. :param session: active user session to use - - :return: the current risk-free rate """ - response = requests.get( - f'{session.base_url}/margin-requirements-public-configuration', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['risk-free-rate'] - return Decimal(data) + data = session.get('/margin-requirements-public-configuration') + return Decimal(data['risk-free-rate']) diff --git a/tastytrade/order.py b/tastytrade/order.py index 70160db..8c2c023 100644 --- a/tastytrade/order.py +++ b/tastytrade/order.py @@ -18,9 +18,11 @@ class InstrumentType(str, Enum): EQUITY = 'Equity' EQUITY_OFFERING = 'Equity Offering' EQUITY_OPTION = 'Equity Option' + FIXED_INCOME = 'Fixed Income Security' FUTURE = 'Future' FUTURE_OPTION = 'Future Option' INDEX = 'Index' + LIQUIDITY_POOL = 'Liquidity Pool' UNKNOWN = 'Unknown' WARRANT = 'Warrant' diff --git a/tastytrade/search.py b/tastytrade/search.py index 4322cca..a5b08da 100644 --- a/tastytrade/search.py +++ b/tastytrade/search.py @@ -1,8 +1,6 @@ from typing import List -import requests - -from tastytrade.session import ProductionSession +from tastytrade.session import Session from tastytrade.utils import TastytradeJsonDataclass @@ -15,26 +13,21 @@ class SymbolData(TastytradeJsonDataclass): def symbol_search( - session: ProductionSession, + session: Session, symbol: str -) -> List[SymbolData]: # pragma: no cover +) -> List[SymbolData]: """ 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 - - :return: a list of symbols and descriptions that match the search phrase """ symbol = symbol.replace('/', '%2F') - response = requests.get( - f'{session.base_url}/symbols/search/{symbol}', - headers=session.headers - ) + response = session.client.get(f'/symbols/search/{symbol}') if response.status_code // 100 != 2: # here it doesn't really make sense to throw an exception return [] else: - data = response.json()['data']['items'] - return [SymbolData(**entry) for entry in data] + data = response.json()['data'] + return [SymbolData(**i) for i in data['items']] diff --git a/tastytrade/session.py b/tastytrade/session.py index 587b4f8..9c0c2ec 100644 --- a/tastytrade/session.py +++ b/tastytrade/session.py @@ -1,8 +1,8 @@ -from abc import ABC from typing import Any, Dict, Optional -import requests +import httpx from fake_useragent import UserAgent # type: ignore +from httpx import Client, Response from tastytrade import API_URL, CERT_URL from tastytrade.utils import (TastytradeError, TastytradeJsonDataclass, @@ -14,126 +14,7 @@ class TwoFactorInfo(TastytradeJsonDataclass): type: Optional[str] = None -class Session(ABC): - """ - An abstract class which contains the basic functionality of a session. - """ - base_url: str - headers: Dict[str, str] - user: Dict[str, str] - session_token: str - - def validate(self) -> bool: - """ - Validates the current session by sending a request to the API. - - :return: True if the session is valid and False otherwise. - """ - 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. - - :return: - True if the session terminated successfully and False otherwise. - """ - response = requests.delete( - f'{self.base_url}/sessions', - headers=self.headers - ) - - return (response.status_code // 100 == 2) - - def get_customer(self) -> Dict[str, Any]: - """ - Gets the customer dict from the API. - - :return: a Tastytrade 'Customer' object in JSON format. - """ - response = requests.get( - f'{self.base_url}/customers/me', - headers=self.headers - ) - validate_response(response) # throws exception if not 200 - - return response.json()['data'] - - -class CertificationSession(Session): - """ - A certification (test) session created at the developer portal which can - be used to interact with the remote API. - - :param login: tastytrade username or email - :param remember_me: - whether or not to create a remember token to use instead of a password - :param password: - tastytrade password to login; if absent, remember token is required - :param remember_token: - previously generated token; if absent, password is required - """ - def __init__( - self, - login: str, - password: Optional[str] = None, - remember_me: bool = False, - remember_token: Optional[str] = None - ): - body = { - 'login': login, - 'remember-me': remember_me - } - if password is not None: - body['password'] = password - elif remember_token is not None: - body['remember-token'] = remember_token - else: - raise TastytradeError('You must provide a password or remember ' - 'token to log in.') - #: The base url to use for API requests - self.base_url: str = CERT_URL - - response = requests.post(f'{self.base_url}/sessions', json=body) - validate_response(response) # throws exception if not 200 - - json = response.json() - #: The user dict returned by the API; contains basic user information - self.user: Dict[str, str] = json['data']['user'] - #: 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 - #: The headers to use for API requests - self.headers: Dict[str, str] = { - 'Accept': 'application/json', - 'Authorization': self.session_token, - 'Content-Type': 'application/json' - } - self.validate() - - # Pull streamer tokens and urls - response = requests.get( - f'{self.base_url}/api-quote-tokens', - headers=self.headers - ) - validate_response(response) - data = response.json()['data'] - self.streamer_token = data['token'] - self.dxlink_url = data['dxlink-url'] - self.streamer_headers = { - 'Authorization': f'Bearer {self.streamer_token}' - } - - -class ProductionSession(Session): +class Session: """ Contains a local user login which can then be used to interact with the remote API. @@ -145,20 +26,32 @@ class ProductionSession(Session): tastytrade password to login; if absent, remember token is required :param remember_token: previously generated token; if absent, password is required + :param is_test: + whether to use the test API endpoints, default False :param two_factor_authentication: if two factor authentication is enabled, this is the code sent to the user's device :param dxfeed_tos_compliant: whether to use the dxfeed TOS-compliant API endpoint for the streamer """ + client: Client + is_test: bool + remember_token: Optional[str] + session_token: str + streamer_token: str + dxlink_url: str + user: Dict[str, str] + def __init__( self, login: str, password: Optional[str] = None, remember_me: bool = False, remember_token: Optional[str] = None, + is_test: bool = False, two_factor_authentication: Optional[str] = None, - dxfeed_tos_compliant: bool = False + dxfeed_tos_compliant: bool = False, + proxy: Optional[str] = None ): body = { 'login': login, @@ -171,68 +64,111 @@ def __init__( else: raise TastytradeError('You must provide a password or remember ' 'token to log in.') - #: The base url to use for API requests - self.base_url: str = API_URL - #: The headers to use for API requests - self.headers: Dict[str, str] = { + # The base url to use for API requests + base_url = CERT_URL if is_test else API_URL + #: Whether this is a cert or real session + self.is_test = is_test + # The headers to use for API requests + headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': UserAgent().random } - if two_factor_authentication is not None: - headers = { - **self.headers, - 'X-Tastyworks-OTP': two_factor_authentication - } - response = requests.post( - f'{self.base_url}/sessions', + response = httpx.post( + f'{base_url}/sessions', json=body, - headers=headers + headers={ + **headers, + 'X-Tastyworks-OTP': two_factor_authentication + }, + proxy=proxy ) else: - response = requests.post(f'{self.base_url}/sessions', json=body) + response = httpx.post( + f'{base_url}/sessions', + json=body, + proxy=proxy + ) validate_response(response) # throws exception if not 200 json = response.json() #: The user dict returned by the API; contains basic user information - self.user: Dict[str, str] = json['data']['user'] + self.user = json['data']['user'] #: The session token used to authenticate requests - self.session_token: str = json['data']['session-token'] - self.headers['Authorization'] = self.session_token + self.session_token = json['data']['session-token'] + headers['Authorization'] = self.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 = json['data'].get('remember-token') + # Set clients for sync and async requests + self.client = Client( + base_url=base_url, + headers=headers, + proxy=proxy, + timeout=30 # many requests can take a while + ) self.validate() # Pull streamer tokens and urls url = ('api-quote-tokens' - if dxfeed_tos_compliant + if dxfeed_tos_compliant or is_test else 'quote-streamer-tokens') - response = requests.get( - f'{self.base_url}/{url}', - headers=self.headers - ) + response = self.client.get(f'/{url}') validate_response(response) data = response.json()['data'] + #: Auth token for dxfeed websocket self.streamer_token = data['token'] + #: URL for dxfeed websocket self.dxlink_url = data['dxlink-url'] - self.streamer_headers = { - 'Authorization': f'Bearer {self.streamer_token}' - } - def get_2fa_info(self) -> TwoFactorInfo: + def get(self, url, **kwargs) -> Dict[str, Any]: + response = self.client.get(url, **kwargs) + return self._validate_and_parse(response) + + def delete(self, url, **kwargs) -> None: + response = self.client.delete(url, **kwargs) + validate_response(response) + + def post(self, url, **kwargs) -> Dict[str, Any]: + response = self.client.post(url, **kwargs) + return self._validate_and_parse(response) + + def put(self, url, **kwargs) -> Dict[str, Any]: + response = self.client.put(url, **kwargs) + return self._validate_and_parse(response) + + def _validate_and_parse(self, response: Response) -> Dict[str, Any]: + validate_response(response) + return response.json()['data'] + + def validate(self) -> bool: """ - Gets the 2FA info for the current user. + Validates the current session by sending a request to the API. - :return: a dictionary containing the 2FA info. + :return: True if the session is valid and False otherwise. """ - response = requests.get( - f'{self.base_url}/users/me/two-factor-method', - headers=self.headers - ) - validate_response(response) + response = self.client.post('/sessions/validate') + return (response.status_code // 100 == 2) - data = response.json()['data'] + def destroy(self) -> None: + """ + Sends a API request to log out of the existing session. This will + invalidate the current session token and login. + """ + self.delete('/sessions') + + def get_customer(self) -> Dict[str, Any]: + """ + Gets the customer dict from the API. + :return: a Tastytrade 'Customer' object in JSON format. + """ + data = self.get('/customers/me') + return data + + def get_2fa_info(self) -> TwoFactorInfo: + """ + Gets the 2FA info for the current user. + """ + data = self.get('/users/me/two-factor-method') return TwoFactorInfo(**data) diff --git a/tastytrade/streamer.py b/tastytrade/streamer.py index a7d0696..745bfbe 100644 --- a/tastytrade/streamer.py +++ b/tastytrade/streamer.py @@ -19,7 +19,7 @@ Underlying) from tastytrade.order import (InstrumentType, OrderChain, PlacedComplexOrder, PlacedOrder, PriceEffect) -from tastytrade.session import CertificationSession, ProductionSession, Session +from tastytrade.session import Session from tastytrade.utils import TastytradeError, TastytradeJsonDataclass from tastytrade.watchlists import Watchlist @@ -123,9 +123,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 - is_certification = isinstance(session, CertificationSession) - self.base_url: str = \ - CERT_STREAMER_URL if is_certification else STREAMER_URL + self.base_url: str = (CERT_STREAMER_URL + if session.is_test else STREAMER_URL) self._queues: Dict[AlertType, Queue] = defaultdict(Queue) self._websocket: Optional[WebSocketClientProtocol] = None @@ -200,7 +199,11 @@ async def listen( while True: yield await self._queues[event_type].get() - async def _map_message(self, type_str: str, data: dict): + async def _map_message( + self, + type_str: str, + data: dict + ): # pragma: no cover """ I'm not sure what the user-status messages look like, so they're absent. @@ -326,7 +329,7 @@ class DXLinkStreamer: """ def __init__( self, - session: ProductionSession, + session: Session, ssl_context: SSLContext = create_default_context() ): self._counter = 0 @@ -368,7 +371,7 @@ async def __aenter__(self): @classmethod async def create( cls, - session: ProductionSession, + session: Session, ssl_context: SSLContext = create_default_context() ) -> 'DXLinkStreamer': self = cls(session, ssl_context=ssl_context) @@ -667,7 +670,7 @@ async def unsubscribe_candle( } await self._websocket.send(json.dumps(message)) - async def _map_message(self, message) -> None: + async def _map_message(self, message) -> None: # pragma: no cover """ Takes the raw JSON data, parses the events and places them into their respective queues. diff --git a/tastytrade/utils.py b/tastytrade/utils.py index 2bf1a78..750649d 100644 --- a/tastytrade/utils.py +++ b/tastytrade/utils.py @@ -2,8 +2,8 @@ import pandas_market_calendars as mcal # type: ignore import pytz +from httpx import Response from pydantic import BaseModel -from requests import Response NYSE = mcal.get_calendar('NYSE') TZ = pytz.timezone('US/Eastern') diff --git a/tastytrade/watchlists.py b/tastytrade/watchlists.py index 463cb83..fa8a7ed 100644 --- a/tastytrade/watchlists.py +++ b/tastytrade/watchlists.py @@ -1,10 +1,8 @@ from typing import Dict, List, Optional -import requests - from tastytrade.instruments import InstrumentType -from tastytrade.session import ProductionSession -from tastytrade.utils import TastytradeJsonDataclass, validate_response +from tastytrade.session import Session +from tastytrade.utils import TastytradeJsonDataclass class Pair(TastytradeJsonDataclass): @@ -30,28 +28,20 @@ class PairsWatchlist(TastytradeJsonDataclass): @classmethod def get_pairs_watchlists( cls, - session: ProductionSession + session: Session ) -> List['PairsWatchlist']: """ Fetches a list of all Tastytrade public pairs watchlists. :param session: the session to use for the request. - - :return: a list of :class:`PairsWatchlist` objects. """ - 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] + data = session.get('/pairs-watchlists') + return [cls(**i) for i in data['items']] @classmethod def get_pairs_watchlist( cls, - session: ProductionSession, + session: Session, name: str ) -> 'PairsWatchlist': """ @@ -59,17 +49,8 @@ def get_pairs_watchlist( :param session: the session to use for the request. :param name: the name of the pairs watchlist to fetch. - - :return: a :class:`PairsWatchlist` object. """ - response = requests.get( - f'{session.base_url}/pairs-watchlists/{name}', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/pairs-watchlists/{name}') return cls(**data) @@ -86,7 +67,7 @@ class Watchlist(TastytradeJsonDataclass): @classmethod def get_public_watchlists( cls, - session: ProductionSession, + session: Session, counts_only: bool = False ) -> List['Watchlist']: """ @@ -94,24 +75,17 @@ def get_public_watchlists( :param session: the session to use for the request. :param counts_only: whether to only fetch the counts of the watchlists. - - :return: a list of :class:`Watchlist` objects. """ - response = requests.get( - f'{session.base_url}/public-watchlists', - headers=session.headers, + data = session.get( + '/public-watchlists', params={'counts-only': counts_only} ) - validate_response(response) - - data = response.json()['data']['items'] - - return [cls(**entry) for entry in data] + return [cls(**i) for i in data['items']] @classmethod def get_public_watchlist( cls, - session: ProductionSession, + session: Session, name: str ) -> 'Watchlist': """ @@ -119,45 +93,27 @@ def get_public_watchlist( :param session: the session to use for the request. :param name: the name of the watchlist to fetch. - - :return: a :class:`Watchlist` object. """ - response = requests.get( - f'{session.base_url}/public-watchlists/{name}', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/public-watchlists/{name}') return cls(**data) @classmethod def get_private_watchlists( cls, - session: ProductionSession + session: Session ) -> List['Watchlist']: """ Fetches a the user's private watchlists. :param session: the session to use for the request. - - :return: a list of :class:`Watchlist` objects. """ - response = requests.get( - f'{session.base_url}/watchlists', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data']['items'] - - return [cls(**entry) for entry in data] + data = session.get('/watchlists') + return [cls(**i) for i in data['items']] @classmethod def get_private_watchlist( cls, - session: ProductionSession, + session: Session, name: str ) -> 'Watchlist': """ @@ -165,62 +121,38 @@ def get_private_watchlist( :param session: the session to use for the request. :param name: the name of the watchlist to fetch. - - :return: a :class:`Watchlist` object. """ - response = requests.get( - f'{session.base_url}/watchlists/{name}', - headers=session.headers - ) - validate_response(response) - - data = response.json()['data'] - + data = session.get(f'/watchlists/{name}') return cls(**data) @classmethod - def remove_private_watchlist( - cls, - session: ProductionSession, - name: str - ) -> None: + def remove_private_watchlist(cls, session: Session, name: str) -> None: """ Deletes the named private watchlist. :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 - ) - validate_response(response) + session.delete(f'/watchlists/{name}') - def upload_private_watchlist(self, session: ProductionSession) -> None: + def upload_private_watchlist(self, session: Session) -> None: """ Creates a private remote watchlist identical to this local one. :param session: the session to use for the request. """ - response = requests.post( - f'{session.base_url}/watchlists', - headers=session.headers, - json=self.dict(by_alias=True) - ) - validate_response(response) + session.post('/watchlists', json=self.model_dump(by_alias=True)) - def update_private_watchlist(self, session: ProductionSession) -> None: + def update_private_watchlist(self, session: Session) -> None: """ Updates the existing private remote watchlist. :param session: the session to use for the request. """ - response = requests.put( - f'{session.base_url}/watchlists/{self.name}', - headers=session.headers, - json=self.dict(by_alias=True) + session.put( + f'/watchlists/{self.name}', + json=self.model_dump(by_alias=True) ) - validate_response(response) def add_symbol(self, symbol: str, instrument_type: InstrumentType) -> None: """ diff --git a/tests/conftest.py b/tests/conftest.py index 869bf6c..f2e80d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ import pytest -from tastytrade import ProductionSession +from tastytrade import Session CERT_USERNAME = 'tastyware' CERT_PASSWORD = ':4s-S9/9L&Q~C]@v' @@ -20,6 +20,6 @@ def session(): assert username is not None assert password is not None - session = ProductionSession(username, password) + session = Session(username, password) yield session session.destroy() diff --git a/tests/test_account.py b/tests/test_account.py index d63ac3a..5ade45c 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -3,7 +3,7 @@ import pytest -from tastytrade import Account, CertificationSession +from tastytrade import Account, Session from tastytrade.instruments import Equity from tastytrade.order import (NewComplexOrder, NewOrder, OrderAction, OrderTimeInForce, OrderType, PriceEffect) @@ -17,7 +17,7 @@ def account(session): @pytest.fixture def cert_session(get_cert_credentials): usr, pwd = get_cert_credentials - session = CertificationSession(usr, pwd) + session = Session(usr, pwd, is_test=True) yield session session.destroy() diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 0000000..b0071b3 --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,14 @@ +from datetime import date + +from tastytrade.search import symbol_search + + +def test_symbol_search_valid(session): + results = symbol_search(session, 'AAP') + symbols = [s.symbol for s in results] + assert 'AAPL' in symbols + + +def test_symbol_search_invalid(session): + results = symbol_search(session, 'ASDFGJKL') + assert results == [] diff --git a/tests/test_session.py b/tests/test_session.py index 427df8d..0338da7 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,4 +1,4 @@ -from tastytrade import CertificationSession +from tastytrade import Session def test_get_customer(session): @@ -7,5 +7,5 @@ def test_get_customer(session): def test_destroy(get_cert_credentials): usr, pwd = get_cert_credentials - session = CertificationSession(usr, pwd) - assert session.destroy() + session = Session(usr, pwd, is_test=True) + session.destroy() From e39b2320f3a5508e89a5f2b8fc4896de8d09167f Mon Sep 17 00:00:00 2001 From: Graeme Holliday Date: Mon, 29 Jul 2024 15:01:06 -0500 Subject: [PATCH 2/4] update docs --- .github/CONTRIBUTING.md | 2 +- README.md | 4 ++-- docs/sessions.rst | 12 ++++++------ docs/watchlists.rst | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index bf0f9ba..44e6662 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributions -Since Tastytrade certification sessions are severely limited in capabilities, the test suite for this SDK requires the usage of your own Tastytrade credentials. In order to pass the tests, you'll need to set use your Tastytrade credentials to run the tests on your local fork +Since Tastytrade certification sessions are severely limited in capabilities, the test suite for this SDK requires the usage of your own Tastytrade credentials. In order to pass the tests, you'll need to set use your Tastytrade credentials to run the tests on your local fork. ## Steps to follow to contribute diff --git a/README.md b/README.md index 1d2afe1..1166a4d 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ A session object is required to authenticate your requests to the Tastytrade API You can create a real session using your normal login, or a certification (test) session using your certification login. ```python -from tastytrade import ProductionSession -session = ProductionSession('username', 'password') +from tastytrade import Session +session = Session('username', 'password') ``` ## Using the streamer diff --git a/docs/sessions.rst b/docs/sessions.rst index 461c850..d3a704c 100644 --- a/docs/sessions.rst +++ b/docs/sessions.rst @@ -9,21 +9,21 @@ To create a production (real) session using your normal login: .. code-block:: python - from tastytrade import ProductionSession - session = ProductionSession('username', 'password') + from tastytrade import Session + session = Session('username', 'password') A certification (test) account can be created `here `_, then used to create a session: .. code-block:: python - from tastytrade import CertificationSession - session = CertificationSession('username', 'password') + from tastytrade import Session + session = Session('username', 'password', is_test=True) You can make a session persistent by generating a remember token, which is valid for 24 hours: .. code-block:: python - session = ProductionSession('username', 'password', remember_me=True) + session = Session('username', 'password', remember_me=True) remember_token = session.remember_token # remember token replaces the password for the next login - new_session = ProductionSession('username', remember_token=remember_token) + new_session = Session('username', remember_token=remember_token) diff --git a/docs/watchlists.rst b/docs/watchlists.rst index ea654ee..93a7555 100644 --- a/docs/watchlists.rst +++ b/docs/watchlists.rst @@ -7,8 +7,8 @@ To use watchlists you'll need a production session: .. code-block:: python - from tastytrade import ProductionSession - session = ProductionSession(user, password) + from tastytrade import Session + session = Session(user, password) Now we can fetch the watchlist: From 80df624eaede91d3393e48118af218d4f9bf5ab5 Mon Sep 17 00:00:00 2001 From: Graeme Holliday Date: Mon, 29 Jul 2024 15:02:00 -0500 Subject: [PATCH 3/4] fix unused import --- tests/test_search.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_search.py b/tests/test_search.py index b0071b3..dc7ce2c 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,5 +1,3 @@ -from datetime import date - from tastytrade.search import symbol_search From d2fb23f8f0d584ae2910bbb2aa25741916fa1462 Mon Sep 17 00:00:00 2001 From: Graeme Holliday Date: Mon, 29 Jul 2024 15:14:41 -0500 Subject: [PATCH 4/4] fix numpy version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 15be76e..e5a4d6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,4 @@ pytest==8.2.1 pytest_cov==5.0.0 pytest-asyncio==0.23.7 fake-useragent==1.5.1 -numpy==1.26.4 \ No newline at end of file +numpy<2 \ No newline at end of file