diff --git a/docs/conf.py b/docs/conf.py index de2a460..4f14967 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'tastytrade' copyright = '2023, Graeme Holliday' author = 'Graeme Holliday' -release = '6.4' +release = '6.5' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/sessions.rst b/docs/sessions.rst index fc93ab0..461c850 100644 --- a/docs/sessions.rst +++ b/docs/sessions.rst @@ -27,56 +27,3 @@ You can make a session persistent by generating a remember token, which is valid remember_token = session.remember_token # remember token replaces the password for the next login new_session = ProductionSession('username', remember_token=remember_token) - -Events ------- - -A ``ProductionSession`` can be used to make simple requests to the dxfeed REST API and pull quotes, greeks and more. -These requests are slower than ``DXFeedStreamer`` and a separate request is required for each event fetched, so they're really more appropriate for a task that just needs to grab some information once. For recurring data feeds/streams or more time-sensitive tasks, the streamer is more appropriate. - -Events are simply market data at a specific timestamp. There's a variety of different kinds of events supported, including: - -- ``Candle`` - Information about open, high, low, and closing prices for an instrument during a certain time range -- ``Greeks`` - (options only) Black-Scholes variables for an option, like delta, gamma, and theta -- ``Quote`` - Live bid and ask prices for an instrument - -Let's look at some examples for these three: - -.. code-block:: python - - from tastytrade.dxfeed import EventType - symbols = ['SPY', 'SPX'] - quotes = session.get_event(EventType.QUOTE, symbols) - print(quotes) - ->>> [Quote(eventSymbol='SPY', eventTime=0, sequence=0, timeNanoPart=0, bidTime=0, bidExchangeCode='Q', bidPrice=411.58, bidSize=400.0, askTime=0, askExchangeCode='Q', askPrice=411.6, askSize=1313.0), Quote(eventSymbol='SPX', eventTime=0, sequence=0, timeNanoPart=0, bidTime=0, bidExchangeCode='\x00', bidPrice=4122.49, bidSize='NaN', askTime=0, askExchangeCode='\x00', askPrice=4123.65, askSize='NaN')] - -To fetch greeks, we need the option symbol in dxfeed format, which can be obtained using :meth:`get_option_chain`: - -.. code-block:: python - - from tastytrade.instruments import get_option_chain - from datetime import date - - chain = get_option_chain(session, 'SPLG') - subs_list = [chain[date(2023, 6, 16)][0].streamer_symbol] - greeks = session.get_event(EventType.GREEKS, subs_list) - print(greeks) - ->>> [Greeks(eventSymbol='.SPLG230616C23', eventTime=0, eventFlags=0, index=7235129486797176832, time=1684559855338, sequence=0, price=26.3380972233688, volatility=0.396983376650804, delta=0.999999999996191, gamma=4.81989763184255e-12, theta=-2.5212017514875e-12, rho=0.01834504287973133, vega=3.7003015672215e-12)] - -Fetching candles requires a bit more info, like the candle width and the start time: - -.. code-block:: python - - from datetime import datetime, timedelta - - subs_list = ['SPY'] - start_time = datetime.now() - timedelta(days=30) # 1 month ago - candles = session.get_candle(subs_list, interval='1d', start_time=start_time) - print(candles[-3:]) - ->>> [Candle(eventSymbol='SPY{=d}', eventTime=0, eventFlags=0, index=7254715159019520000, time=1689120000000, sequence=0, count=142679, open=446.39, high=447.4799, low=444.91, close=446.02, volume=91924527, vwap=445.258750197419, bidVolume=14787054, askVolume=15196448, impVolatility='NaN', openInterest='NaN'), Candle(eventSymbol='SPY{=d}', eventTime=0, eventFlags=0, index=7255086244193894400, time=1689206400000, sequence=0, count=106759, open=447.9, high=450.38, low=447.45, close=449.56, volume=72425241, vwap=448.163832976481, bidVolume=10384321, askVolume=11120400, impVolatility='NaN', openInterest='NaN'), Candle(eventSymbol='SPY{=d}', eventTime=0, eventFlags=0, index=7255457329368268800, time=1689292800000, sequence=0, count=113369, open=450.475, high=451.36, low=448.49, close=449.28, volume=69815823, vwap=449.948156765549, bidVolume=10905920, askVolume=13136337, impVolatility='NaN', openInterest='NaN')] diff --git a/setup.py b/setup.py index 80654cb..bbe7ff4 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='tastytrade', - version='6.4', + version='6.5', description='An unofficial SDK for Tastytrade!', long_description=LONG_DESCRIPTION, long_description_content_type='text/x-rst', diff --git a/tastytrade/__init__.py b/tastytrade/__init__.py index 2c2de28..b731a2d 100644 --- a/tastytrade/__init__.py +++ b/tastytrade/__init__.py @@ -2,7 +2,7 @@ API_URL = 'https://api.tastyworks.com' CERT_URL = 'https://api.cert.tastyworks.com' -VERSION = '6.4.1' +VERSION = '6.5' logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) diff --git a/tastytrade/instruments.py b/tastytrade/instruments.py index e31bdf4..f65c6c6 100644 --- a/tastytrade/instruments.py +++ b/tastytrade/instruments.py @@ -1,3 +1,4 @@ +import re from datetime import date, datetime from decimal import Decimal from enum import Enum @@ -477,7 +478,33 @@ def _set_streamer_symbol(self) -> None: exp = self.expiration_date.strftime('%y%m%d') self.streamer_symbol = \ - f".{self.underlying_symbol}{exp}{self.option_type.value}{strike}" + f'.{self.underlying_symbol}{exp}{self.option_type.value}{strike}' + + @classmethod + 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+))?', + streamer_symbol + ) + if match is None: + return '' + symbol = match.group(1)[:6].ljust(6) + exp = datetime.strptime(match.group(2), '%y%m%d').strftime('%Y%m%d') + option_type = match.group(3) + strike = match.group(4).zfill(5) + if match.group(6) is not None: + decimal = str(100 * int(match.group(6))).zfill(3) + else: + decimal = '000' + + return f'{symbol}{exp}{option_type}{strike}{decimal}' class NestedOptionChain(TastytradeJsonDataclass): diff --git a/tastytrade/metrics.py b/tastytrade/metrics.py index 41f01e7..e378c3b 100644 --- a/tastytrade/metrics.py +++ b/tastytrade/metrics.py @@ -24,6 +24,22 @@ class EarningsInfo(TastytradeJsonDataclass): eps: Decimal +class EarningsReport(TastytradeJsonDataclass): + """ + Dataclass containing information about a recent earnings report, or the + expected date of the next one. + """ + estimated: bool + late_flag: int + visible: bool + actual_eps: Optional[Decimal] = None + consensus_estimate: Optional[Decimal] = None + expected_report_date: Optional[date] = None + quarter_end_date: Optional[date] = None + time_of_day: Optional[str] = None + updated_at: Optional[datetime] = None + + class Liquidity(TastytradeJsonDataclass): """ Dataclass representing liquidity information for a given symbol. @@ -67,6 +83,7 @@ class MarketMetricInfo(TastytradeJsonDataclass): beta: Optional[Decimal] = None corr_spy_3month: Optional[Decimal] = None market_cap: Decimal + earnings: Optional[EarningsReport] = None price_earnings_ratio: Optional[Decimal] = None earnings_per_share: Optional[Decimal] = None dividend_rate_per_share: Optional[Decimal] = None diff --git a/tastytrade/session.py b/tastytrade/session.py index 4f100ac..29dca5f 100644 --- a/tastytrade/session.py +++ b/tastytrade/session.py @@ -1,13 +1,9 @@ from abc import ABC -from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional import requests from tastytrade import API_URL, CERT_URL -from tastytrade.dxfeed import (Candle, Event, EventType, Greeks, Profile, - Quote, Summary, TheoPrice, TimeAndSale, Trade, - Underlying) from tastytrade.utils import TastytradeError, validate_response @@ -189,164 +185,3 @@ def __init__( self.streamer_headers = { 'Authorization': f'Bearer {self.streamer_token}' } - - def get_candle( - self, - symbols: List[str], - interval: str, - start_time: datetime, - end_time: Optional[datetime] = None, - extended_trading_hours: bool = False - ) -> List[Candle]: - """ - Using the dxfeed REST API, fetchs Candle events for the given list of - symbols. - - This is meant for single-use requests. If you need a fast, recurring - datastream, use :class:`tastytrade.streamer.Streamer` instead. - - :param symbols: the list of symbols to fetch the event for - :param interval: - the width of each candle in time, e.g. '15s', '5m', '1h', '3d', - '1w', '1mo' - :param start_time: starting time for the data range - :param end_time: ending time for the data range - :param extended_trading_hours: whether to include extended trading - - :return: a list of Candle events - """ - candle_str = f'{{={interval},tho=true}}' \ - if extended_trading_hours else f'{{={interval}}}' - params = { - 'events': EventType.CANDLE, - 'symbols': (candle_str + ',').join(symbols) + candle_str, - 'fromTime': int(start_time.timestamp() * 1000) - } - if end_time is not None: - params['toTime'] = int(end_time.timestamp() * 1000) - response = requests.get( - self.rest_url, - headers=self.streamer_headers, - params=params # type: ignore - ) - validate_response(response) # throws exception if not 200 - - data = response.json()[EventType.CANDLE] - candles = [] - for _, v in data.items(): - candles.extend([Candle(**d) for d in v]) - - return candles - - def get_event( - self, - event_type: EventType, - symbols: List[str], - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None - ) -> List[Event]: - """ - Using the dxfeed REST API, fetches an event for the given list of - symbols. For `EventType.CANDLE`, use :meth:`get_candle` instead, and - :meth:`get_time_and_sale` for `EventType.TIME_AND_SALE`. - - This is meant for single-use requests. If you need a fast, recurring - datastream, use :class:`tastytrade.streamer.Streamer` instead. - - :param event_type: the type of event to fetch - :param symbols: the list of symbols to fetch the event for - :param start_time: the start time of the event - :param end_time: the end time of the event - - :return: a list of events - """ - # this shouldn't be called with candle - if event_type == EventType.CANDLE: - raise TastytradeError('Invalid event type for `get_event`: Use ' - '`get_candle` instead!') - params: Dict[str, Any] = { - 'events': event_type, - 'symbols': ','.join(symbols) - } - if start_time is not None: - params['fromTime'] = int(start_time.timestamp() * 1000) - if end_time is not None: - params['toTime'] = int(end_time.timestamp() * 1000) - response = requests.get( - self.rest_url, - headers=self.streamer_headers, - params=params - ) - validate_response(response) # throws exception if not 200 - - data = response.json()[event_type] - - return [_map_event(event_type, v) for _, v in data.items()] - - def get_time_and_sale( - self, - symbols: List[str], - start_time: datetime, - end_time: Optional[datetime] = None - ) -> List[TimeAndSale]: - """ - Using the dxfeed REST API, fetchs TimeAndSale events for the given - list of symbols. - - This is meant for single-use requests. If you need a fast, recurring - datastream, use :class:`tastytrade.streamer.Streamer` instead. - - :param symbols: the list of symbols to fetch the event for - :param start_time: the start time of the event - :param end_time: the end time of the event - - :return: a list of TimeAndSale events - """ - params = { - 'events': EventType.TIME_AND_SALE, - 'symbols': ','.join(symbols), - 'fromTime': int(start_time.timestamp() * 1000) - } - if end_time is not None: - params['toTime'] = int(end_time.timestamp() * 1000) - response = requests.get( - self.rest_url, - headers=self.streamer_headers, - params=params # type: ignore - ) - validate_response(response) # throws exception if not 200 - - data = response.json()[EventType.TIME_AND_SALE] - tas = [] - for symbol in symbols: - tas.extend([TimeAndSale(**d) for d in data[symbol]]) - - return tas - - -def _map_event( - event_type: str, - event_dict: Any # Usually Dict[str, Any]; sometimes a list -) -> Event: # pragma: no cover - """ - Parses the raw JSON data from the dxfeed REST API into event objects. - - :param event_type: the type of event to map to - :param event_dict: the raw JSON data from the dxfeed REST API - """ - if event_type == EventType.GREEKS: - return Greeks(**event_dict[0]) - elif event_type == EventType.PROFILE: - return Profile(**event_dict) - elif event_type == EventType.QUOTE: - return Quote(**event_dict) - elif event_type == EventType.SUMMARY: - return Summary(**event_dict) - elif event_type == EventType.THEO_PRICE: - return TheoPrice(**event_dict[0]) - elif event_type == EventType.TRADE: - return Trade(**event_dict) - elif event_type == EventType.UNDERLYING: - return Underlying(**event_dict[0]) # type: ignore - else: - raise TastytradeError(f'Unknown event type: {event_type}') diff --git a/tests/test_instruments.py b/tests/test_instruments.py index 8f32a98..71f48d5 100644 --- a/tests/test_instruments.py +++ b/tests/test_instruments.py @@ -80,3 +80,9 @@ def test_get_future_option_chain(session): FutureOption.get_future_option(session, options[0].symbol) FutureOption.get_future_options(session, options[:4]) break + + +def test_streamer_symbol_to_occ(): + dxf = '.SPY240324P480.5' + occ = 'SPY 20240324P00480500' + assert Option.streamer_symbol_to_occ(dxf) == occ diff --git a/tests/test_session.py b/tests/test_session.py index e8d8406..427df8d 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,27 +1,10 @@ -from datetime import datetime, timedelta - from tastytrade import CertificationSession -from tastytrade.dxfeed import EventType def test_get_customer(session): assert session.get_customer() != {} -def test_get_event(session): - session.get_event(EventType.QUOTE, ['SPY', 'AAPL']) - - -def test_get_time_and_sale(session): - start_date = datetime.today() - timedelta(days=30) - session.get_time_and_sale(['SPY', 'AAPL'], start_date) - - -def test_get_candle(session): - start_date = datetime.today() - timedelta(days=30) - session.get_candle(['SPY', 'AAPL'], '1d', start_date) - - def test_destroy(get_cert_credentials): usr, pwd = get_cert_credentials session = CertificationSession(usr, pwd)