Skip to content

Commit

Permalink
better typing for streamer, orders (#170)
Browse files Browse the repository at this point in the history
* better typing for streamer, orders

* update docs

* fix lint

* use Self type, guarantee streamer_symbol on some instruments
  • Loading branch information
Graeme22 authored Oct 10, 2024
1 parent 4be4016 commit 2c903c6
Show file tree
Hide file tree
Showing 18 changed files with 304 additions and 437 deletions.
5 changes: 1 addition & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: install lint test docs
.PHONY: install lint test

install:
uv sync
Expand All @@ -10,6 +10,3 @@ lint:

test:
uv run pytest --cov=tastytrade --cov-report=term-missing tests/ --cov-fail-under=95

docs:
cd docs; uv pip install -r requirements.txt; make html
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ The streamer is a websocket connection to dxfeed (the Tastytrade data provider)

```python
from tastytrade import DXLinkStreamer
from tastytrade.dxfeed import EventType
from tastytrade.dxfeed import Quote

async with DXLinkStreamer(session) as streamer:
subs_list = ['SPY'] # list of symbols to subscribe to
await streamer.subscribe(EventType.QUOTE, subs_list)
await streamer.subscribe(Quote, subs_list)
# this example fetches quotes once, then exits
quote = await streamer.get_event(EventType.QUOTE)
quote = await streamer.get_event(Quote)
print(quote)
```

Expand Down Expand Up @@ -67,7 +67,7 @@ print(positions[0])
from decimal import Decimal
from tastytrade import Account
from tastytrade.instruments import Equity
from tastytrade.order import NewOrder, OrderAction, OrderTimeInForce, OrderType, PriceEffect
from tastytrade.order import NewOrder, OrderAction, OrderTimeInForce, OrderType

account = Account.get_account(session, '5WX01234')
symbol = Equity.get_equity(session, 'USO')
Expand All @@ -77,8 +77,7 @@ order = NewOrder(
time_in_force=OrderTimeInForce.DAY,
order_type=OrderType.LIMIT,
legs=[leg], # you can have multiple legs in an order
price=Decimal('10'), # limit price, $10/share for a total value of $50
price_effect=PriceEffect.DEBIT
price=Decimal('-10') # limit price, $10/share debit for a total value of $50
)
response = account.place_order(session, order, dry_run=True) # a test order
print(response)
Expand All @@ -92,6 +91,7 @@ print(response)

```python
from tastytrade import DXLinkStreamer
from tastytrade.dxfeed import Greeks
from tastytrade.instruments import get_option_chain
from tastytrade.utils import get_tasty_monthly

Expand All @@ -100,8 +100,8 @@ exp = get_tasty_monthly() # 45 DTE expiration!
subs_list = [chain[exp][0].streamer_symbol]

async with DXLinkStreamer(session) as streamer:
await streamer.subscribe(EventType.GREEKS, subs_list)
greeks = await streamer.get_event(EventType.GREEKS)
await streamer.subscribe(Greeks, subs_list)
greeks = await streamer.get_event(Greeks)
print(greeks)
```

Expand All @@ -110,7 +110,7 @@ async with DXLinkStreamer(session) as streamer:
```

For more examples, check out the [documentation](https://tastyworks-api.readthedocs.io/en/latest/).

## Disclaimer

This is an unofficial SDK for Tastytrade. There is no implied warranty for any actions and results which arise from using it.
Expand Down
13 changes: 7 additions & 6 deletions docs/account-streamer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ Here's an example of setting up an account streamer to continuously wait for eve

.. code-block:: python
from tastytrade import Account, AlertStreamer
from tastytrade.streamer import AlertType
from tastytrade import Account, AlertStreamer, Watchlist
async with AlertStreamer(session) as streamer:
accounts = Account.get_accounts(session)
Expand All @@ -21,16 +20,18 @@ Here's an example of setting up an account streamer to continuously wait for eve
# quote alerts configured by the user
await streamer.subscribe_quote_alerts()
async for watchlist in streamer.listen(AlertType.WATCHLIST):
print(f'Watchlist updated: {watchlist}')
async for wl in streamer.listen(Watchlist):
print(wl)
Probably the most important information the account streamer handles is order fills. We can listen just for orders like so:

.. code-block:: python
from tastytrade.order import PlacedOrder
async with AlertStreamer(session) as streamer:
accounts = Account.get_accounts(session)
await streamer.subscribe_accounts(accounts)
async for order in streamer.listen(AlertType.ORDER):
print(f'Order updated: {order}')
async for order in streamer.listen(PlacedOrder):
print(order)
25 changes: 13 additions & 12 deletions docs/data-streamer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ Once you've created the streamer, you can subscribe/unsubscribe to events, like

.. code-block:: python
from tastytrade.dxfeed import EventType
from tastytrade.dxfeed import Quote
subs_list = ['SPY', 'SPX']
async with DXLinkStreamer(session) as streamer:
await streamer.subscribe(EventType.QUOTE, subs_list)
await streamer.subscribe(Quote, subs_list)
quotes = {}
async for quote in streamer.listen(EventType.QUOTE):
async for quote in streamer.listen(Quote):
quotes[quote.eventSymbol] = quote
if len(quotes) >= len(subs_list):
break
Expand All @@ -45,20 +45,21 @@ Note that these are ``asyncio`` calls, so you'll need to run this code asynchron
import asyncio
async def main(session):
async with DXLinkStreamer(session) as streamer:
await streamer.subscribe(EventType.QUOTE, subs_list)
quote = await streamer.get_event(EventType.QUOTE)
await streamer.subscribe(Quote, subs_list)
quote = await streamer.get_event(Quote)
print(quote)
asyncio.run(main(session))
>>> [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')]

Alternatively, you can do testing in a Jupyter notebook, which allows you to make async calls directly.
Alternatively, you can do testing in a Jupyter notebook, which allows you to make async calls directly, or run a python shell like this: `python -m asyncio`.

We can also use the streamer to stream greeks for options symbols:

.. code-block:: python
from tastytrade.dxfeed import Greeks
from tastytrade.instruments import get_option_chain
from tastytrade.utils import get_tasty_monthly
Expand All @@ -67,8 +68,8 @@ We can also use the streamer to stream greeks for options symbols:
subs_list = [chain[exp][0].streamer_symbol]
async with DXLinkStreamer(session) as streamer:
await streamer.subscribe(EventType.GREEKS, subs_list)
greeks = await streamer.get_event(EventType.GREEKS)
await streamer.subscribe(Greeks, subs_list)
greeks = await streamer.get_event(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)]
Expand Down Expand Up @@ -111,8 +112,8 @@ For example, we can use the streamer to create an option chain that will continu
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)
await streamer.subscribe(Quote, [symbol] + streamer_symbols)
await streamer.subscribe(Greeks, streamer_symbols)
puts = [o for o in options if o.option_type == OptionType.PUT]
calls = [o for o in options if o.option_type == OptionType.CALL]
Expand All @@ -129,11 +130,11 @@ For example, we can use the streamer to create an option chain that will continu
return self
async def _update_greeks(self):
async for e in self.streamer.listen(EventType.GREEKS):
async for e in self.streamer.listen(Greeks):
self.greeks[e.eventSymbol] = e
async def _update_quotes(self):
async for e in self.streamer.listen(EventType.QUOTE):
async for e in self.streamer.listen(Quote):
self.quotes[e.eventSymbol] = e
Now, we can access the quotes and greeks at any time, and they'll be up-to-date with the live prices from the streamer:
Expand Down
14 changes: 14 additions & 0 deletions docs/dxfeed.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ Summary
:members:
:show-inheritance:

TimeAndSale
-----------

.. automodule:: tastytrade.dxfeed.timeandsale
:members:
:show-inheritance:

TheoPrice
---------

Expand All @@ -61,3 +68,10 @@ Trade
.. automodule:: tastytrade.dxfeed.trade
:members:
:show-inheritance:

Underlying
----------

.. automodule:: tastytrade.dxfeed.underlying
:members:
:show-inheritance:
5 changes: 4 additions & 1 deletion docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ If you want to build the documentation (usually not necessary):

::

$ make docs
$ source .venv/bin/activate
$ cd docs
$ uv pip install -r requirements.txt
$ make html

Windows
-------
Expand Down
3 changes: 1 addition & 2 deletions docs/instruments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ This makes placing new trades across a wide variety of instruments surprisingly
put.build_leg(Decimal(1), OrderAction.SELL_TO_OPEN),
call.build_leg(Decimal(1), OrderAction.SELL_TO_OPEN)
],
price=Decimal('1.25'), # price is always per quantity, not total
price_effect=PriceEffect.CREDIT
price=Decimal('1.25') # price is always per quantity, not total
)
# assuming an initialized account
account.place_order(session, order, dry_run=False)
Expand Down
16 changes: 6 additions & 10 deletions docs/orders.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ Placing an order
time_in_force=OrderTimeInForce.DAY,
order_type=OrderType.LIMIT,
legs=[leg], # you can have multiple legs in an order
price=Decimal('10'), # limit price, $10/share for a total value of $50
price_effect=PriceEffect.DEBIT
price=Decimal('-10') # limit price, $10/share debit for a total value of $50
)
response = account.place_order(session, order, dry_run=True) # a test order
print(response)
Expand All @@ -29,7 +28,7 @@ Placing an order
Notice the use of the ``dry_run`` parameter in the call to ``place_order``. This is used to calculate the effects that an order would have on the account's buying power and the fees that would be charged without actually placing the order. This is typically used to provide an order confirmation screen before sending the order.
To send the order, pass ``dry_run=False``, and the response will be populated with a ``PlacedOrderResponse``, which contains information about the order and account.
Also, due to the quirks of the Tastytrade API, the limit price for a stock order is the price per share, whereas the limit price for an options order is the total price.
Also, rather than using an explicit credit/debit toggle like the Tastytrade platform, the SDK simply assumes negative numbers are debits and positive ones are credits.

Managing orders
---------------
Expand All @@ -38,7 +37,7 @@ Once we've placed an order, it's often necessary to modify or cancel the order f

.. code-block:: python
previous_order.price = Decimal('10.05') # let's increase the price to get a fill!
previous_order.price = Decimal('-10.05') # let's pay more to get a fill!
response = account.replace_order(session, previous_response.order.id, previous_order)
Cancelling an order is similar:
Expand Down Expand Up @@ -81,23 +80,20 @@ To create an OTOCO order, you need an entry point order, a stop loss order, and
time_in_force=OrderTimeInForce.DAY,
order_type=OrderType.LIMIT,
legs=[opening],
price=Decimal('180'),
price_effect=PriceEffect.DEBIT
price=Decimal('-180') # open for $180 debit
),
orders=[
NewOrder(
time_in_force=OrderTimeInForce.GTC,
order_type=OrderType.LIMIT,
legs=[closing],
price=Decimal('200'), # take profits
price_effect=PriceEffect.CREDIT
price=Decimal('200') # take profits
),
NewOrder(
time_in_force=OrderTimeInForce.GTC,
order_type=OrderType.STOP,
legs=[closing],
stop_trigger=Decimal('160'), # stop loss
price_effect=PriceEffect.CREDIT
stop_trigger=Decimal('160') # stop loss
)
]
)
Expand Down
Loading

0 comments on commit 2c903c6

Please sign in to comment.