Skip to content

Commit

Permalink
remove dxfeed REST; dxfeed to occ helper; add earnings (#120)
Browse files Browse the repository at this point in the history
* remove dxfeed REST; dxfeed to occ helper; add earnings
* remove dxfeed REST from docs
  • Loading branch information
Graeme22 authored Jan 29, 2024
1 parent 8d5cc78 commit a2d6ad0
Show file tree
Hide file tree
Showing 9 changed files with 55 additions and 240 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 0 additions & 53 deletions docs/sessions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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')]
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tastytrade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 28 additions & 1 deletion tastytrade/instruments.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from datetime import date, datetime
from decimal import Decimal
from enum import Enum
Expand Down Expand Up @@ -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):
Expand Down
17 changes: 17 additions & 0 deletions tastytrade/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
167 changes: 1 addition & 166 deletions tastytrade/session.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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}')
6 changes: 6 additions & 0 deletions tests/test_instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 0 additions & 17 deletions tests/test_session.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down

0 comments on commit a2d6ad0

Please sign in to comment.