Skip to content

Commit

Permalink
add user agent; process compact data
Browse files Browse the repository at this point in the history
  • Loading branch information
Graeme22 committed Apr 19, 2024
1 parent e5a180e commit fda4b2a
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 413 deletions.
13 changes: 6 additions & 7 deletions docs/data-streamer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ Or, you can create a streamer using an asynchronous context manager:
async with DXLinkStreamer(session) as streamer:
pass
There are two kinds of streamers: ``DXLinkStreamer`` and ``DXFeedStreamer``. ``DXFeedStreamer`` is older, but has been kept around for compatibility reasons. It supports more event types, but it's now deprecated as it will probably be moved to delayed quotes at some point.
Once you've created the streamer, you can subscribe/unsubscribe to events, like ``Quote``:

.. code-block:: python
from tastytrade.dxfeed import EventType
subs_list = ['SPY', 'SPX']
async with DXFeedStreamer(session) as streamer:
async with DXLinkStreamer(session) as streamer:
await streamer.subscribe(EventType.QUOTE, subs_list)
quotes = {}
async for quote in streamer.listen(EventType.QUOTE):
Expand Down Expand Up @@ -67,7 +66,7 @@ We can also use the streamer to stream greeks for options symbols:
exp = get_tasty_monthly() # 45 DTE expiration!
subs_list = [chain[exp][0].streamer_symbol]
async with DXFeedStreamer(session) as streamer:
async with DXLinkStreamer(session) as streamer:
await streamer.subscribe(EventType.GREEKS, subs_list)
greeks = await streamer.get_event(EventType.GREEKS)
print(greeks)
Expand All @@ -85,7 +84,7 @@ For example, we can use the streamer to create an option chain that will continu
import asyncio
from datetime import date
from dataclasses import dataclass
from tastytrade import DXFeedStreamer
from tastytrade import DXLinkStreamer
from tastytrade.instruments import get_option_chain
from tastytrade.dxfeed import Greeks, Quote
from tastytrade.utils import today_in_new_york
Expand All @@ -94,14 +93,14 @@ For example, we can use the streamer to create an option chain that will continu
class LivePrices:
quotes: dict[str, Quote]
greeks: dict[str, Greeks]
streamer: DXFeedStreamer
streamer: DXLinkStreamer
puts: list[Option]
calls: list[Option]
@classmethod
async def create(
cls,
session: ProductionSession,
session: Session,
symbol: str = 'SPY',
expiration: date = today_in_new_york()
):
Expand All @@ -110,7 +109,7 @@ For example, we can use the streamer to create an option chain that will continu
# the `streamer_symbol` property is the symbol used by the streamer
streamer_symbols = [o.streamer_symbol for o in options]
streamer = await DXFeedStreamer.create(session)
streamer = await DXLinkStreamer.create(session)
# subscribe to quotes and greeks for all options on that date
await streamer.subscribe(EventType.QUOTE, [symbol] + streamer_symbols)
await streamer.subscribe(EventType.GREEKS, streamer_symbols)
Expand Down
2 changes: 1 addition & 1 deletion docs/instruments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Alternatively, ``NestedOptionChain`` and ``NestedFutureOptionChain`` provide a s
chain = NestedOptionChain.get_chain(session, 'SPY')
print(chain.expirations[0].strikes[0])
>>> Strike(strike_price=Decimal('415.0'), call='SPY 240207C00415000', put='SPY 240207P00415000')
>>> Strike(strike_price=Decimal('437.0'), call='SPY 240417C00437000', put='SPY 240417P00437000', call_streamer_symbol='.SPY240417C437', put_streamer_symbol='.SPY240417P437')

Each expiration contains a list of these strikes, which have the associated put and call symbols that can then be used to fetch option objects via ``Option.get_options()`` or converted to dxfeed symbols for use with the streamer via ``Option.occ_to_streamer_symbol()``.

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pydantic==2.6.3
pytest==7.4.0
pytest_cov==4.1.0
pytest-asyncio==0.21.1
fake-useragent==1.5.1
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
'requests<3',
'websockets>=11.0.3',
'pydantic>=2.6.3',
'pandas_market_calendars>=4.3.3'
'pandas_market_calendars>=4.3.3',
'fake_useragent>=1.5.1',
],
packages=find_packages(exclude=['ez_setup', 'tests*']),
include_package_data=True
Expand Down
4 changes: 1 addition & 3 deletions tastytrade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@
from .account import Account # noqa: E402
from .search import symbol_search # noqa: E402
from .session import CertificationSession, ProductionSession # noqa: E402
from .streamer import (AccountStreamer, DXFeedStreamer, # noqa: E402
DXLinkStreamer)
from .streamer import AccountStreamer, DXLinkStreamer # noqa: E402
from .watchlists import PairsWatchlist, Watchlist # noqa: E402

__all__ = [
'Account',
'AccountStreamer',
'CertificationSession',
'DXFeedStreamer',
'DXLinkStreamer',
'PairsWatchlist',
'ProductionSession',
Expand Down
14 changes: 0 additions & 14 deletions tastytrade/dxfeed/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from enum import Enum

from .candle import Candle
from .event import Event, EventType
from .greeks import Greeks
Expand All @@ -24,15 +22,3 @@
'Trade',
'Underlying'
]


class Channel(str, Enum):
"""
This is an :class:`~enum.Enum` that contains the channels for the quote
streamer.
"""
DATA = '/service/data'
HANDSHAKE = '/meta/handshake'
HEARTBEAT = '/meta/connect'
SUBSCRIPTION = '/service/sub'
TIME_SERIES = '/service/timeSeriesData'
4 changes: 3 additions & 1 deletion tastytrade/dxfeed/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ def from_stream(cls, data: list) -> List['Event']: # pragma: no cover
if not multiples.is_integer():
msg = 'Mapper data input values are not a multiple of the key size'
raise TastytradeError(msg)
keys = cls.model_fields.keys()
for i in range(int(multiples)):
offset = i * size
local_values = data[offset:(i + 1) * size]
objs.append(cls(*local_values))
event_dict = dict(zip(keys, local_values))
objs.append(cls(**event_dict))
return objs
48 changes: 42 additions & 6 deletions tastytrade/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
from typing import Any, Dict, Optional

import requests
from fake_useragent import UserAgent # type: ignore

from tastytrade import API_URL, CERT_URL
from tastytrade.utils import TastytradeError, validate_response
from tastytrade.utils import (TastytradeError, TastytradeJsonDataclass,
validate_response)


class TwoFactorInfo(TastytradeJsonDataclass):
is_active: bool
type: Optional[str] = None


class Session(ABC):
Expand Down Expand Up @@ -108,6 +115,19 @@ def __init__(
self.headers: Dict[str, str] = {'Authorization': self.session_token}
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):
"""
Expand Down Expand Up @@ -167,21 +187,37 @@ def __init__(
self.remember_token: Optional[str] = \
json['data']['remember-token'] if remember_me else None
#: The headers to use for API requests
self.headers: Dict[str, str] = {'Authorization': self.session_token}
self.headers: Dict[str, str] = {
'Authorization': self.session_token,
'User-Agent': UserAgent().random
}
self.validate()

#: Pull streamer tokens and urls
# Pull streamer tokens and urls
response = requests.get(
f'{self.base_url}/quote-streamer-tokens',
headers=self.headers
)
validate_response(response)
data = response.json()['data']
self.streamer_token = data['token']
url = data['websocket-url'] + '/cometd'
self.dxfeed_url = url.replace('https', 'wss')
self.dxlink_url = data['dxlink-url']
self.rest_url = data['websocket-url'] + '/rest/events.json'
self.streamer_headers = {
'Authorization': f'Bearer {self.streamer_token}'
}

def get_2fa_info(self) -> TwoFactorInfo:
"""
Gets the 2FA info for the current user.
:return: a dictionary containing the 2FA info.
"""
response = requests.get(
f'{self.base_url}/users/me/two-factor-method',
headers=self.headers
)
validate_response(response)

data = response.json()['data']

return TwoFactorInfo(**data)
Loading

0 comments on commit fda4b2a

Please sign in to comment.