Skip to content

Commit

Permalink
dxfeed streamer removed; cert/real sessions combined (#140)
Browse files Browse the repository at this point in the history
* remove dxfeed streamer; new streamer URL; use same class for prod/cert sessions

* update docs

* update readme to handle github breakage

* use compact data format for streamer

* relax coverage, add 2fa

* fix isort

* fix candle bug
  • Loading branch information
Graeme22 authored Apr 25, 2024
1 parent e5a180e commit c0e24cf
Show file tree
Hide file tree
Showing 23 changed files with 247 additions and 577 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
mypy -p tastytrade
- name: Testing...
run: |
python -m pytest --cov=tastytrade --cov-report=term-missing tests/ --cov-fail-under=95
python -m pytest --cov=tastytrade --cov-report=term-missing tests/ --cov-fail-under=90
env:
TT_USERNAME: ${{ secrets.TT_USERNAME }}
TT_PASSWORD: ${{ secrets.TT_PASSWORD }}
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ lint:
mypy -p tests

test:
python -m pytest --cov=tastytrade --cov-report=term-missing tests/ --cov-fail-under=95
python -m pytest --cov=tastytrade --cov-report=term-missing tests/ --cov-fail-under=90

install:
env/bin/pip install -e .
Expand Down
20 changes: 14 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ You can create a real session using your normal login, or a certification (test)

.. code-block:: python
from tastytrade import ProductionSession
session = ProductionSession('username', 'password')
from tastytrade import Session
session = Session('username', 'password')
Using the streamer
------------------
Expand All @@ -51,7 +51,9 @@ The streamer is a websocket connection to dxfeed (the Tastytrade data provider)
quote = await streamer.get_event(EventType.QUOTE)
print(quote)
>>> [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')]
.. code-block:: bash
>>> [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')]
Note that this is asynchronous code, so you can't run it as is unless you're using a Jupyter notebook or something similar.

Expand All @@ -66,7 +68,9 @@ Getting current positions
positions = account.get_positions(session)
print(positions[0])
>>> CurrentPosition(account_number='5WX01234', symbol='IAU', instrument_type=<InstrumentType.EQUITY: 'Equity'>, underlying_symbol='IAU', quantity=Decimal('20'), quantity_direction='Long', close_price=Decimal('37.09'), average_open_price=Decimal('37.51'), average_yearly_market_close_price=Decimal('37.51'), average_daily_market_close_price=Decimal('37.51'), multiplier=1, cost_effect='Credit', is_suppressed=False, is_frozen=False, realized_day_gain=Decimal('7.888'), realized_day_gain_effect='Credit', realized_day_gain_date=datetime.date(2023, 5, 19), realized_today=Decimal('0.512'), realized_today_effect='Debit', realized_today_date=datetime.date(2023, 5, 19), created_at=datetime.datetime(2023, 3, 31, 14, 38, 32, 58000, tzinfo=datetime.timezone.utc), updated_at=datetime.datetime(2023, 5, 19, 16, 56, 51, 920000, tzinfo=datetime.timezone.utc), mark=None, mark_price=None, restricted_quantity=Decimal('0'), expires_at=None, fixing_price=None, deliverable_type=None)
.. code-block:: bash
>>> CurrentPosition(account_number='5WX01234', symbol='IAU', instrument_type=<InstrumentType.EQUITY: 'Equity'>, underlying_symbol='IAU', quantity=Decimal('20'), quantity_direction='Long', close_price=Decimal('37.09'), average_open_price=Decimal('37.51'), average_yearly_market_close_price=Decimal('37.51'), average_daily_market_close_price=Decimal('37.51'), multiplier=1, cost_effect='Credit', is_suppressed=False, is_frozen=False, realized_day_gain=Decimal('7.888'), realized_day_gain_effect='Credit', realized_day_gain_date=datetime.date(2023, 5, 19), realized_today=Decimal('0.512'), realized_today_effect='Debit', realized_today_date=datetime.date(2023, 5, 19), created_at=datetime.datetime(2023, 3, 31, 14, 38, 32, 58000, tzinfo=datetime.timezone.utc), updated_at=datetime.datetime(2023, 5, 19, 16, 56, 51, 920000, tzinfo=datetime.timezone.utc), mark=None, mark_price=None, restricted_quantity=Decimal('0'), expires_at=None, fixing_price=None, deliverable_type=None)
Placing an order
----------------
Expand All @@ -92,7 +96,9 @@ Placing an order
response = account.place_order(session, order, dry_run=True) # a test order
print(response)
>>> PlacedOrderResponse(buying_power_effect=BuyingPowerEffect(change_in_margin_requirement=Decimal('125.0'), change_in_margin_requirement_effect=<PriceEffect.DEBIT: 'Debit'>, change_in_buying_power=Decimal('125.004'), change_in_buying_power_effect=<PriceEffect.DEBIT: 'Debit'>, current_buying_power=Decimal('1000.0'), current_buying_power_effect=<PriceEffect.CREDIT: 'Credit'>, new_buying_power=Decimal('874.996'), new_buying_power_effect=<PriceEffect.CREDIT: 'Credit'>, isolated_order_margin_requirement=Decimal('125.0'), isolated_order_margin_requirement_effect=<PriceEffect.DEBIT: 'Debit'>, is_spread=False, impact=Decimal('125.004'), effect=<PriceEffect.DEBIT: 'Debit'>), fee_calculation=FeeCalculation(regulatory_fees=Decimal('0.0'), regulatory_fees_effect=<PriceEffect.NONE: 'None'>, clearing_fees=Decimal('0.004'), clearing_fees_effect=<PriceEffect.DEBIT: 'Debit'>, commission=Decimal('0.0'), commission_effect=<PriceEffect.NONE: 'None'>, proprietary_index_option_fees=Decimal('0.0'), proprietary_index_option_fees_effect=<PriceEffect.NONE: 'None'>, total_fees=Decimal('0.004'), total_fees_effect=<PriceEffect.DEBIT: 'Debit'>), order=PlacedOrder(account_number='5WV69754', time_in_force=<OrderTimeInForce.DAY: 'Day'>, order_type=<OrderType.LIMIT: 'Limit'>, size='5', underlying_symbol='USO', underlying_instrument_type=<InstrumentType.EQUITY: 'Equity'>, status=<OrderStatus.RECEIVED: 'Received'>, cancellable=True, editable=True, edited=False, updated_at=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), legs=[Leg(instrument_type=<InstrumentType.EQUITY: 'Equity'>, symbol='USO', action=<OrderAction.BUY_TO_OPEN: 'Buy to Open'>, quantity=Decimal('5'), remaining_quantity=Decimal('5'), fills=[])], id=None, price=Decimal('50.0'), price_effect=<PriceEffect.DEBIT: 'Debit'>, gtc_date=None, value=None, value_effect=None, stop_trigger=None, contingent_status=None, confirmation_status=None, cancelled_at=None, cancel_user_id=None, cancel_username=None, replacing_order_id=None, replaces_order_id=None, in_flight_at=None, live_at=None, received_at=None, reject_reason=None, user_id=None, username=None, terminal_at=None, complex_order_id=None, complex_order_tag=None, preflight_id=None, order_rule=None), complex_order=None, warnings=[Message(code='tif_next_valid_sesssion', message='Your order will begin working during next valid session.', preflight_id=None)], errors=None)
.. code-block:: bash
>>> PlacedOrderResponse(buying_power_effect=BuyingPowerEffect(change_in_margin_requirement=Decimal('125.0'), change_in_margin_requirement_effect=<PriceEffect.DEBIT: 'Debit'>, change_in_buying_power=Decimal('125.004'), change_in_buying_power_effect=<PriceEffect.DEBIT: 'Debit'>, current_buying_power=Decimal('1000.0'), current_buying_power_effect=<PriceEffect.CREDIT: 'Credit'>, new_buying_power=Decimal('874.996'), new_buying_power_effect=<PriceEffect.CREDIT: 'Credit'>, isolated_order_margin_requirement=Decimal('125.0'), isolated_order_margin_requirement_effect=<PriceEffect.DEBIT: 'Debit'>, is_spread=False, impact=Decimal('125.004'), effect=<PriceEffect.DEBIT: 'Debit'>), fee_calculation=FeeCalculation(regulatory_fees=Decimal('0.0'), regulatory_fees_effect=<PriceEffect.NONE: 'None'>, clearing_fees=Decimal('0.004'), clearing_fees_effect=<PriceEffect.DEBIT: 'Debit'>, commission=Decimal('0.0'), commission_effect=<PriceEffect.NONE: 'None'>, proprietary_index_option_fees=Decimal('0.0'), proprietary_index_option_fees_effect=<PriceEffect.NONE: 'None'>, total_fees=Decimal('0.004'), total_fees_effect=<PriceEffect.DEBIT: 'Debit'>), order=PlacedOrder(account_number='5WV69754', time_in_force=<OrderTimeInForce.DAY: 'Day'>, order_type=<OrderType.LIMIT: 'Limit'>, size='5', underlying_symbol='USO', underlying_instrument_type=<InstrumentType.EQUITY: 'Equity'>, status=<OrderStatus.RECEIVED: 'Received'>, cancellable=True, editable=True, edited=False, updated_at=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), legs=[Leg(instrument_type=<InstrumentType.EQUITY: 'Equity'>, symbol='USO', action=<OrderAction.BUY_TO_OPEN: 'Buy to Open'>, quantity=Decimal('5'), remaining_quantity=Decimal('5'), fills=[])], id=None, price=Decimal('50.0'), price_effect=<PriceEffect.DEBIT: 'Debit'>, gtc_date=None, value=None, value_effect=None, stop_trigger=None, contingent_status=None, confirmation_status=None, cancelled_at=None, cancel_user_id=None, cancel_username=None, replacing_order_id=None, replaces_order_id=None, in_flight_at=None, live_at=None, received_at=None, reject_reason=None, user_id=None, username=None, terminal_at=None, complex_order_id=None, complex_order_tag=None, preflight_id=None, order_rule=None), complex_order=None, warnings=[Message(code='tif_next_valid_sesssion', message='Your order will begin working during next valid session.', preflight_id=None)], errors=None)
Options chain/streaming greeks
------------------------------
Expand All @@ -112,7 +118,9 @@ Options chain/streaming greeks
greeks = await streamer.get_event(EventType.GREEKS)
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)]
.. code-block:: bash
>>> [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)]
For more examples, check out the `documentation <https://tastyworks-api.readthedocs.io/en/latest/>`_.

Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = 'tastytrade'
copyright = '2023, Graeme Holliday'
copyright = '2024, Graeme Holliday'
author = 'Graeme Holliday'
release = '7.1'
release = '8.0'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
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
13 changes: 7 additions & 6 deletions docs/sessions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,22 @@ 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 <https://developer.tastytrade.com/sandbox/>`_, then used to create a session:

.. code-block:: python
from tastytrade import CertificationSession
session = CertificationSession('username', 'password')
session = Session('username', 'password', is_test=True)
Be aware that not all endpoints work with certification sessions.

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)
4 changes: 2 additions & 2 deletions docs/watchlists.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
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='7.1',
version='8.0',
description='An unofficial SDK for Tastytrade!',
long_description=LONG_DESCRIPTION,
long_description_content_type='text/x-rst',
Expand Down
11 changes: 4 additions & 7 deletions tastytrade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@

API_URL = 'https://api.tastyworks.com'
CERT_URL = 'https://api.cert.tastyworks.com'
VERSION = '7.1'
VERSION = '8.0'

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 (AccountStreamer, DXFeedStreamer, # noqa: E402
DXLinkStreamer)
from .session import Session # noqa: E402
from .streamer import AccountStreamer, DXLinkStreamer # noqa: E402
from .watchlists import PairsWatchlist, Watchlist # noqa: E402

__all__ = [
'Account',
'AccountStreamer',
'CertificationSession',
'DXFeedStreamer',
'DXLinkStreamer',
'PairsWatchlist',
'ProductionSession',
'Session',
'Watchlist',
'symbol_search'
]
6 changes: 3 additions & 3 deletions tastytrade/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from tastytrade.order import (InstrumentType, NewComplexOrder, NewOrder,
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)

Expand Down Expand Up @@ -723,7 +723,7 @@ def get_total_fees(

def get_net_liquidating_value_history(
self,
session: ProductionSession,
session: Session,
time_back: Optional[str] = None,
start_time: Optional[datetime] = None
) -> List[NetLiqOhlc]:
Expand Down Expand Up @@ -786,7 +786,7 @@ def get_position_limit(self, session: Session) -> PositionLimit:

def get_effective_margin_requirements(
self,
session: ProductionSession,
session: Session,
symbol: str
) -> MarginRequirement:
"""
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'
6 changes: 3 additions & 3 deletions tastytrade/dxfeed/candle.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ class Candle(Event):
#: the last (close) price of the candle
close: Optional[Decimal] = None
#: the total volume of the candle
volume: Optional[int] = None
volume: Optional[Decimal] = None
#: volume-weighted average price
vwap: Optional[Decimal] = None
#: bid volume in the candle
bidVolume: Optional[int] = None
bidVolume: Optional[Decimal] = None
#: ask volume in the candle
askVolume: Optional[int] = None
askVolume: Optional[Decimal] = None
#: implied volatility in the candle
impVolatility: Optional[Decimal] = None
#: open interest in the candle
Expand Down
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
4 changes: 2 additions & 2 deletions tastytrade/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import requests

from tastytrade.order import InstrumentType, TradeableTastytradeJsonDataclass
from tastytrade.session import ProductionSession, Session
from tastytrade.session import Session
from tastytrade.utils import TastytradeJsonDataclass, validate_response


Expand Down Expand Up @@ -1119,7 +1119,7 @@ def get_option_chain(


def get_future_option_chain(
session: ProductionSession,
session: Session,
symbol: str
) -> Dict[date, List[FutureOption]]:
"""
Expand Down
Loading

0 comments on commit c0e24cf

Please sign in to comment.