From 737096fddb7b3531ed27a250a0bdebd225664796 Mon Sep 17 00:00:00 2001 From: Graeme Holliday Date: Wed, 7 Feb 2024 12:35:56 -0500 Subject: [PATCH] docs-update --- README.rst | 21 ++++--- docs/account-streamer.rst | 15 +++++ docs/accounts.rst | 29 ++++++++-- docs/data-streamer.rst | 18 +++--- docs/img/netliq.png | Bin 0 -> 22084 bytes docs/instruments.rst | 115 ++++++++++++++++++++++++++++++++++---- docs/orders.rst | 35 +++++++++++- tastytrade/instruments.py | 28 ++++++++++ tests/test_account.py | 10 ++-- tests/test_instruments.py | 6 ++ 10 files changed, 237 insertions(+), 40 deletions(-) create mode 100644 docs/img/netliq.png diff --git a/README.rst b/README.rst index 9e641a1..4097ef7 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,8 @@ The streamer is a websocket connection to dxfeed (the Tastytrade data provider) >>> [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. + Getting current positions ------------------------- @@ -63,7 +65,7 @@ Getting current positions positions = account.get_positions(session) print(positions[0]) ->>> CurrentPosition(account_number='5WV69754', symbol='IAU', instrument_type=, 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) +>>> CurrentPosition(account_number='5WX01234', symbol='IAU', instrument_type=, 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 ---------------- @@ -75,7 +77,7 @@ Placing an order from tastytrade.instruments import Equity from tastytrade.order import NewOrder, OrderAction, OrderTimeInForce, OrderType, PriceEffect - account = Account.get_account(session, '5WV69754') + account = Account.get_account(session, '5WX01234') symbol = Equity.get_equity(session, 'USO') leg = symbol.build_leg(Decimal('5'), OrderAction.BUY_TO_OPEN) # buy to open 5 shares @@ -83,7 +85,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('50'), # limit price, here $50 for 5 shares = $10/share + price=Decimal('10'), # limit price, $10/share for a total value of $50 price_effect=PriceEffect.DEBIT ) response = account.place_order(session, order, dry_run=True) # a test order @@ -96,15 +98,18 @@ Options chain/streaming greeks .. code-block:: python + from tastytrade import DXLinkStreamer from tastytrade.instruments import get_option_chain - from datetime import date + from tastytrade.utils import get_tasty_monthly chain = get_option_chain(session, 'SPLG') - subs_list = [chain[date(2023, 6, 16)][0].streamer_symbol] + exp = get_tasty_monthly() # 45 DTE expiration! + subs_list = [chain[exp][0].streamer_symbol] - await streamer.subscribe(EventType.GREEKS, subs_list) - greeks = await streamer.get_event(EventType.GREEKS) - print(greeks) + async with DXLinkStreamer(session) as streamer: + await streamer.subscribe(EventType.GREEKS, subs_list) + 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)] diff --git a/docs/account-streamer.rst b/docs/account-streamer.rst index 3cc0105..c6a87bd 100644 --- a/docs/account-streamer.rst +++ b/docs/account-streamer.rst @@ -22,3 +22,18 @@ Here's an example of setting up an account streamer to continuously wait for eve async for data in streamer.listen(): print(data) + +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 def listen_for_orders(session): + async with AccountStreamer(session) as streamer: + accounts = Account.get_accounts(session) + await streamer.subscribe_accounts(accounts) + + async for data in streamer.listen(): + if isinstance(data, PlacedOrder): + yield return data diff --git a/docs/accounts.rst b/docs/accounts.rst index 0496cd5..1030456 100644 --- a/docs/accounts.rst +++ b/docs/accounts.rst @@ -34,11 +34,28 @@ To obtain information about current positions: >>> CurrentPosition(account_number='5WX01234', symbol='BRK/B', instrument_type=, underlying_symbol='BRK/B', quantity=Decimal('10'), quantity_direction='Long', close_price=Decimal('361.34'), average_open_price=Decimal('339.63'), multiplier=1, cost_effect='Credit', is_suppressed=False, is_frozen=False, realized_day_gain=Decimal('18.5'), realized_today=Decimal('279.15'), created_at=datetime.datetime(2023, 3, 31, 14, 35, 40, 138000, tzinfo=datetime.timezone.utc), updated_at=datetime.datetime(2023, 8, 10, 15, 42, 7, 482000, tzinfo=datetime.timezone.utc), mark=None, mark_price=None, restricted_quantity=Decimal('0'), expires_at=None, fixing_price=None, deliverable_type=None, average_yearly_market_close_price=Decimal('339.63'), average_daily_market_close_price=Decimal('361.34'), realized_day_gain_effect=, realized_day_gain_date=datetime.date(2023, 8, 10), realized_today_effect=, realized_today_date=datetime.date(2023, 8, 10)) -TODO: -get_history -get_net_liquidating_value_history -get_live_orders -delete_order(and place and replace) -get_order_history +To fetch a list of past transactions: + +.. code-block:: python + + history = account.get_history(session, start_date=date(2024, 1, 1)) + print(history[-1]) + +>>> Transaction(id=280070508, account_number='5WX01234', transaction_type='Trade', transaction_sub_type='Sell to Close', description='Sold 10 BRK/B @ 384.04', executed_at=datetime.datetime(2024, 1, 26, 15, 51, 53, 685000, tzinfo=datetime.timezone.utc), transaction_date=datetime.date(2024, 1, 26), value=Decimal('3840.4'), value_effect=, net_value=Decimal('3840.35'), net_value_effect=, is_estimated_fee=True, symbol='BRK/B', instrument_type=, underlying_symbol='BRK/B', action='Sell to Close', quantity=Decimal('10.0'), price=Decimal('384.04'), regulatory_fees=Decimal('0.042'), regulatory_fees_effect=, clearing_fees=Decimal('0.008'), clearing_fees_effect=, commission=Decimal('0.0'), commission_effect=, proprietary_index_option_fees=Decimal('0.0'), proprietary_index_option_fees_effect=, ext_exchange_order_number='12271026815307', ext_global_order_number=2857, ext_group_id='0', ext_group_fill_id='0', ext_exec_id='0', exec_id='123_40126000126350300000', exchange='JNS', order_id=305250635, exchange_affiliation_identifier='', leg_count=1, destination_venue='JANE_STREET_EQUITIES_A', other_charge=None, other_charge_effect=None, other_charge_description=None, reverses_id=None, cost_basis_reconciliation_date=None, lots=None, agency_price=None, principal_price=None) + +We can also view portfolio P/L over time (and even plot it!): + +.. code-block:: python + + import matplotlib.pyplot as plt + nl = account.get_net_liquidating_value_history(session, time_back='1m') # past 1 month + plt.plot([n.time for n in nl], [n.close for n in nl]) + plt.show() + +.. image:: img/netliq.png + :width: 640 + :alt: P/L graph + +Accounts are needed to place, replace, and delete orders. See more in :doc:`Orders `. There are many more things you can do with an ``Account`` object--check out the SDK Reference section! \ No newline at end of file diff --git a/docs/data-streamer.rst b/docs/data-streamer.rst index 293a338..92c4877 100644 --- a/docs/data-streamer.rst +++ b/docs/data-streamer.rst @@ -30,26 +30,27 @@ Once you've created the streamer, you can subscribe/unsubscribe to events, like async with DXFeedStreamer(session) as streamer: await streamer.subscribe(EventType.QUOTE, subs_list) - quotes = [] + quotes = {} async for quote in streamer.listen(EventType.QUOTE): - quotes.append(quote) + quotes[quote.eventSymbol] = quote if len(quotes) >= len(subs_list): break 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')] +>>> {'SPY': 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), 'SPX': 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 these are ``asyncio`` calls, so you'll need to run this code asynchronously. Here's an example: .. code-block:: python - async def main(): + 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) print(quote) - - asyncio.run(main()) + + 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')] @@ -60,10 +61,11 @@ We can also use the streamer to stream greeks for options symbols: .. code-block:: python from tastytrade.instruments import get_option_chain - from datetime import date + from tastytrade.utils import get_tasty_monthly chain = get_option_chain(session, 'SPLG') - subs_list = [chain[date(2023, 6, 16)][0].streamer_symbol] + exp = get_tasty_monthly() # 45 DTE expiration! + subs_list = [chain[exp][0].streamer_symbol] async with DXFeedStreamer(session) as streamer: await streamer.subscribe(EventType.GREEKS, subs_list) diff --git a/docs/img/netliq.png b/docs/img/netliq.png new file mode 100644 index 0000000000000000000000000000000000000000..71145d69a7454a2df752c7709f8b88b7433986a2 GIT binary patch literal 22084 zcmeFZby$?^zdriTkfMYlg0zC5ASES@A~8rvcPL7ibTby95(?5C(%sD{3Zir>H6W5h zN%x%R<-7LxxA%3Oz0dE&pJ!dyT8pWt-%s54{drzLP*Wr)qbGwPh#aMKPZNTWXb3_~ zkrIPr8T_crgXo2=Gu zZqBaae0&c7{sOO)ixnU1;mH@!$QfrP16K&5dlfgqhS)V({}UP&tx-hSHK zP58gNk0axk&lp{J2c3VXbFuLLmU7vH=W~;`Mh*JCy13_>=N(>+rn;Ne8p<10X6RZ+ zbB~TP8L51%J#Znv%koYa)1G!V%BT)8t7bC-PA6&X z?Cnqi1u&k{u$M}r;Q!F^TZj~b%moQ$!HdKz5E6oZoPx8|MrTpE7T*(3iI+%hr4Tf3JO7t6pC4i1Q7JxJXAwo{#6h;L-tET z-{H@sXsOabe8BAdykh-j2x2|wySqBHKHbVT5gNh@?Z|?kX3fdS#>dB>cO-`((xCPk zjevmWZQt>5@Y5-VovzMKcCdR?7R!?UKT-&=@ZKr>zudcf{*ZCKTOK1t@FgC@!ByWj zx@7;IUoN*94`x{Kbn8u0zB{?KwfcE^dEYxa`nE^RO4{1m&=w;0KhtamvO88h>^kEF z46AH)unbOeG7Bn}TGKe}3mINN# z6&Y43i%z&jxpx+uG-w+e7gltO-yf@X7|4)+>$dnY)^?)a-G7(`lEn+QEu;qqRjiS%G#L+&&!n=Tv{ZGq&9;N0|r45V`e@Eh0B9O?G#}GnGRnE{)}AibNcLv{_~Ut6X}G0ZPLc z2CltZHQ8X*uX{rWXQ*iN@$O-D};CxP7$XZBgRGCMn4br@h{ zlNT;?;x#)rXIMf*L(?(ycOltxvMk)jcO@g^YIIDD$7ptay|}kz3YrpX8!qgN;W4TNt4h%lxKcH;v^AhUQa7J6Hog1f`*-B$RCXM$yu6$@;;)xv zeqNr2va)gmZqk2uqJDeWNN$x9Vs-ic-L9}FOn^y^3AxO%#D#_rnDB^k=1_qix0qPn z#4i45UdHbDVB2Tv8OcGDgXp;Pi|;>v6h$=o?(X<9Kv25a15-PH(~+MSKgB?OY)i!zET3wFo?Chi{2#oyW(GVRUe|a4Ng*XOFx)09}ZBLR`6NWHrh!RS= z&Z#(Ho290z-tsl;&$LI3L|(fu
Y^7JbW0xNT}Hv=~3)^QL1D1tq&wDf`Jq<0Pl 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 (False, 'APPLE INC') +>>> 'CME' + +The different instruments have a host of properties that will be automatically populated with their associated values upon initialization. You can explore these properties in depth in the "SDK Reference" section. + +Options chains +-------------- + +The symbol structure for options and futures options is somewhat complex, so you can use ``get_option_chain()`` and ``get_future_option_chain()`` to get the instruments for a specific underlying as explained below. + +.. code-block:: python + + from tastytrade.instruments import get_option_chain, get_future_option_chain + from tastytrade.utils import get_tasty_monthly + + chain = get_option_chain(session, 'SPLG') + exp = get_tasty_monthly() # 45 DTE expiration! + print(chain[exp][0]) + future_chain = get_future_option_chain(session, '/MCL') + print(future_chain.keys()) # print all expirations + +>>> instrument_type= symbol='SPLG 240315C00024000' active=True strike_price=Decimal('24.0') root_symbol='SPLG' underlying_symbol='SPLG' expiration_date=datetime.date(2024, 3, 15) exercise_style='American' shares_per_contract=100 option_type= option_chain_type='Standard' expiration_type='Regular' settlement_type='PM' stops_trading_at=datetime.datetime(2024, 3, 15, 20, 0, tzinfo=datetime.timezone.utc) market_time_instrument_collection='Equity Option' days_to_expiration=38 expires_at=datetime.datetime(2024, 3, 15, 20, 0, tzinfo=datetime.timezone.utc) is_closing_only=False listed_market=None halted_at=None old_security_number=None streamer_symbol='.SPLG240315C24' +>>> dict_keys([datetime.date(2024, 7, 17), datetime.date(2024, 6, 14), datetime.date(2024, 9, 17), datetime.date(2024, 11, 15), datetime.date(2024, 12, 16), datetime.date(2024, 2, 9), datetime.date(2024, 5, 16), datetime.date(2025, 1, 15), datetime.date(2024, 8, 15), datetime.date(2024, 2, 16), datetime.date(2024, 2, 14), datetime.date(2024, 10, 17), datetime.date(2024, 4, 17), datetime.date(2024, 3, 15)]) + +Alternatively, ``NestedOptionChain`` and ``NestedFutureOptionChain`` provide a structured way to fetch chain expirations and available strikes. + +.. code-block:: python + + from tastytrade.instruments import NestedOptionChain + + chain = NestedOptionChain.get_chain(session, 'SPY') + print(chain.expirations[0].strikes[0]) + +>>> Strike(strike_price=Decimal('415.0'), call='SPY 240207C00415000', put='SPY 240207P00415000') + +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()``. + +Placing trades +-------------- + +Probably the most powerful tool available for instruments is the ``build_leg()`` function. This allows an instrument to be quickly converted into a tradeable 'leg', which by itself or together with other legs forms the basis for a trade. +This makes placing new trades across a wide variety of instruments surprisingly simple: + +.. code-block:: python + + from tastytrade.instruments import get_future_option_chain + from tastytrade.order import * + from datetime import date + + chain = get_future_option_chain(session, '/MCL') + put = chain[date(2024, 3, 15)][286] + call = chain[date(2024, 3, 15)][187] + + order = NewOrder( + time_in_force=OrderTimeInForce.DAY, + order_type=OrderType.LIMIT, + legs=[ + # two parameters: quantity and order action + 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 + ) + # assuming an initialized account + account.place_order(session, order, dry_run=False) + +That's it! We just sold a micro crude oil futures strangle in a few lines of code. +Note that price is per quantity, not the price for the entire order! So if the legs looked like this: + +.. code-block:: python + + legs=[ + put.build_leg(Decimal(2), OrderAction.SELL_TO_OPEN), + call.build_leg(Decimal(2), OrderAction.SELL_TO_OPEN) + ] + +the price would still be ``Decimal('1.25')``, and the total credit collected would be $2.50. This holds true for ratio spreads, so a 4:2 ratio spread should be priced as a 2:1 ratio spread. diff --git a/docs/orders.rst b/docs/orders.rst index 1a5fee9..7258803 100644 --- a/docs/orders.rst +++ b/docs/orders.rst @@ -11,7 +11,7 @@ Placing an order from tastytrade.instruments import Equity from tastytrade.order import * - account = Account.get_account(session, '5WV69754') + account = Account.get_account(session, '5WX01234') symbol = Equity.get_equity(session, 'USO') leg = symbol.build_leg(Decimal('5'), OrderAction.BUY_TO_OPEN) # buy to open 5 shares @@ -19,7 +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('50'), # limit price, here $50 for 5 shares = $10/share + price=Decimal('10'), # limit price, $10/share for a total value of $50 price_effect=PriceEffect.DEBIT ) response = account.place_order(session, order, dry_run=True) # a test order @@ -29,6 +29,35 @@ 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. + +Managing orders +--------------- + +Once we've placed an order, it's often necessary to modify or cancel the order for a variety of reasons. Thankfully, this is easy and handled through the ``Account`` object: + +.. code-block:: python + + previous_order.price = Decimal('10.05') # let's increase the price to get a fill! + response = account.replace_order(session, previous_response.order.id, previous_order) + +Cancelling an order is similar: + +.. code-block:: python + + account.delete_order(session, placed_order.id) + +Placed orders are assigned a status, like "Received", "Cancelled", or "Filled". To watch for status changes in real time, you can use the :doc:`Account Streamer `. +To get current order status, you can just call ``get_live_orders``. (The name is somewhat misleading! It returns not only live orders, but also cancelled and filled ones over the past 24 hours.) + +.. code-block:: python + + orders = account.get_live_orders(session) + print(orders) + +>>> [PlacedOrder(account_number='5WX01234', time_in_force=, order_type=, underlying_symbol='SPY', underlying_instrument_type=, status=, cancellable=False, editable=False, edited=False, updated_at=datetime.datetime(2024, 2, 6, 0, 2, 56, 559000, tzinfo=datetime.timezone.utc), legs=[Leg(instrument_type=, symbol='SPY', action=, quantity=Decimal('1'), remaining_quantity=Decimal('1'), fills=[])], size='1', id='306731648', price=Decimal('40.0'), price_effect=, gtc_date=None, value=None, value_effect=None, stop_trigger=None, contingent_status=None, confirmation_status=None, cancelled_at=datetime.datetime(2024, 2, 6, 0, 2, 56, 548000, tzinfo=datetime.timezone.utc), cancel_user_id=None, cancel_username=None, replacing_order_id=None, replaces_order_id=None, in_flight_at=None, live_at=None, received_at=datetime.datetime(2024, 2, 6, 0, 2, 55, 347000, tzinfo=datetime.timezone.utc), reject_reason=None, user_id=None, username=None, terminal_at=datetime.datetime(2024, 2, 6, 0, 2, 56, 548000, tzinfo=datetime.timezone.utc), complex_order_id=None, complex_order_tag=None, preflight_id=None, order_rule=None), PlacedOrder(account_number='5WX01234', time_in_force=, order_type=, underlying_symbol='SPY', underlying_instrument_type=, status=, cancellable=False, editable=False, edited=True, updated_at=datetime.datetime(2024, 2, 6, 0, 2, 55, 362000, tzinfo=datetime.timezone.utc), legs=[Leg(instrument_type=, symbol='SPY', action=, quantity=Decimal('1'), remaining_quantity=Decimal('1'), fills=[])], size='1', id='306731647', price=Decimal('42.0'), price_effect=, gtc_date=None, value=None, value_effect=None, stop_trigger=None, contingent_status=None, confirmation_status=None, cancelled_at=datetime.datetime(2024, 2, 6, 0, 2, 55, 341000, tzinfo=datetime.timezone.utc), cancel_user_id=None, cancel_username=None, replacing_order_id=None, replaces_order_id=None, in_flight_at=None, live_at=None, received_at=datetime.datetime(2024, 2, 6, 0, 2, 54, 781000, tzinfo=datetime.timezone.utc), reject_reason=None, user_id=None, username=None, terminal_at=datetime.datetime(2024, 2, 6, 0, 2, 55, 341000, tzinfo=datetime.timezone.utc), complex_order_id=None, complex_order_tag=None, preflight_id=None, order_rule=None), PlacedOrder(account_number='5WX01234', time_in_force=, order_type=, underlying_symbol='SPY', underlying_instrument_type=, status=, cancellable=False, editable=False, edited=False, updated_at=datetime.datetime(2024, 2, 6, 0, 2, 54, 433000, tzinfo=datetime.timezone.utc), legs=[Leg(instrument_type=, symbol='SPY', action=, quantity=Decimal('1'), remaining_quantity=Decimal('1'), fills=[])], size='1', id='306731645', price=Decimal('42.0'), price_effect=, gtc_date=None, value=None, value_effect=None, stop_trigger=None, contingent_status=None, confirmation_status=None, cancelled_at=datetime.datetime(2024, 2, 6, 0, 2, 54, 422000, tzinfo=datetime.timezone.utc), cancel_user_id=None, cancel_username=None, replacing_order_id=None, replaces_order_id=None, in_flight_at=None, live_at=None, received_at=datetime.datetime(2024, 2, 6, 0, 2, 53, 203000, tzinfo=datetime.timezone.utc), reject_reason=None, user_id=None, username=None, terminal_at=datetime.datetime(2024, 2, 6, 0, 2, 54, 422000, tzinfo=datetime.timezone.utc), complex_order_id=None, complex_order_tag=None, preflight_id=None, order_rule=None), PlacedOrder(account_number='5WX01234', time_in_force=, order_type=, underlying_symbol='SPY', underlying_instrument_type=, status=, cancellable=False, editable=False, edited=False, updated_at=datetime.datetime(2024, 2, 5, 23, 46, 44, 844000, tzinfo=datetime.timezone.utc), legs=[Leg(instrument_type=, symbol='SPY', action=, quantity=Decimal('1'), remaining_quantity=Decimal('1'), fills=[])], size='1', id='306731381', price=Decimal('40.0'), price_effect=, gtc_date=None, value=None, value_effect=None, stop_trigger=None, contingent_status=None, confirmation_status=None, cancelled_at=datetime.datetime(2024, 2, 5, 23, 46, 44, 833000, tzinfo=datetime.timezone.utc), cancel_user_id=None, cancel_username=None, replacing_order_id=None, replaces_order_id=None, in_flight_at=None, live_at=None, received_at=datetime.datetime(2024, 2, 5, 23, 46, 43, 150000, tzinfo=datetime.timezone.utc), reject_reason=None, user_id=None, username=None, terminal_at=datetime.datetime(2024, 2, 5, 23, 46, 44, 833000, tzinfo=datetime.timezone.utc), complex_order_id=None, complex_order_tag=None, preflight_id=None, order_rule=None), PlacedOrder(account_number='5WX01234', time_in_force=, order_type=, underlying_symbol='SPY', underlying_instrument_type=, status=, cancellable=False, editable=False, edited=True, updated_at=datetime.datetime(2024, 2, 5, 23, 46, 43, 183000, tzinfo=datetime.timezone.utc), legs=[Leg(instrument_type=, symbol='SPY', action=, quantity=Decimal('1'), remaining_quantity=Decimal('1'), fills=[])], size='1', id='306731380', price=Decimal('42.0'), price_effect=, gtc_date=None, value=None, value_effect=None, stop_trigger=None, contingent_status=None, confirmation_status=None, cancelled_at=datetime.datetime(2024, 2, 5, 23, 46, 43, 145000, tzinfo=datetime.timezone.utc), cancel_user_id=None, cancel_username=None, replacing_order_id=None, replaces_order_id=None, in_flight_at=None, live_at=None, received_at=datetime.datetime(2024, 2, 5, 23, 46, 41, 647000, tzinfo=datetime.timezone.utc), reject_reason=None, user_id=None, username=None, terminal_at=datetime.datetime(2024, 2, 5, 23, 46, 43, 145000, tzinfo=datetime.timezone.utc), complex_order_id=None, complex_order_tag=None, preflight_id=None, order_rule=None)] + +For less recent orders, we can get the full order history with ``get_order_history``. Complex Orders -------------- @@ -100,3 +129,5 @@ An OCO order is similar, but has no trigger order. It's used to add a profit-tak ] ) resp = account.place_complex_order(session, oco, dry_run=False) + +Note that to cancel complex orders, you need to use the ``delete_complex_order`` function, NOT ``delete_order``. diff --git a/tastytrade/instruments.py b/tastytrade/instruments.py index 1fb2fe0..abc0922 100644 --- a/tastytrade/instruments.py +++ b/tastytrade/instruments.py @@ -506,6 +506,34 @@ def streamer_symbol_to_occ(cls, streamer_symbol) -> str: return f'{symbol}{exp}{option_type}{strike}{decimal}' + @classmethod + def occ_to_streamer_symbol(cls, occ) -> str: + """ + Returns the dxfeed symbol for use in the streamer from the given OCC + 2010 symbol. + + :param occ: the OCC symbol to convert + + :return: the equivalent streamer symbol + """ + match = re.match( + r'([A-Z]+)\s+(\d{6})([CP])(\d{5})(\d{3})', + occ + ) + if match is None: + return '' + symbol = match.group(1) + exp = match.group(2) + option_type = match.group(3) + strike = int(match.group(4)) + decimal = int(match.group(5)) + + res = f'.{symbol}{exp}{option_type}{strike}' + if decimal != 0: + decimal_str = str(decimal / 1000.0) + res += decimal_str[1:] + return res + class NestedOptionChain(TastytradeJsonDataclass): """ diff --git a/tests/test_account.py b/tests/test_account.py index bfc6caf..02019d3 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -25,7 +25,7 @@ def cert_session(get_cert_credentials): @pytest.fixture(scope='session') def cert_account(cert_session): - return Account.get_accounts(cert_session)[0] + return Account.get_accounts(cert_session)[1] def test_get_account(session, account): @@ -94,7 +94,7 @@ def placed_order(session, account, new_order): def test_place_and_delete_order(session, account, new_order): order = account.place_order(session, new_order, dry_run=False).order - sleep(1) + sleep(3) account.delete_order(session, order.id) @@ -106,7 +106,7 @@ def test_replace_and_delete_order(session, account, new_order, placed_order): modified_order = new_order.copy() modified_order.price = Decimal(40) replaced = account.replace_order(session, placed_order.id, modified_order) - sleep(1) + sleep(3) account.delete_order(session, replaced.id) @@ -153,7 +153,7 @@ def test_place_oco_order(cert_session, cert_account): ] ) resp2 = account.place_complex_order(session, oco, dry_run=False) - sleep(1) + sleep(3) # test get complex order _ = account.get_complex_order(session, resp2.complex_order.id) account.delete_complex_order(session, resp2.complex_order.id) @@ -191,5 +191,5 @@ def test_place_otoco_order(cert_session, cert_account): ] ) resp = account.place_complex_order(session, otoco, dry_run=False) - sleep(1) + sleep(3) account.delete_complex_order(session, resp.complex_order.id) diff --git a/tests/test_instruments.py b/tests/test_instruments.py index 92e7075..bcc37fb 100644 --- a/tests/test_instruments.py +++ b/tests/test_instruments.py @@ -86,3 +86,9 @@ def test_streamer_symbol_to_occ(): dxf = '.SPY240324P480.5' occ = 'SPY 240324P00480500' assert Option.streamer_symbol_to_occ(dxf) == occ + + +def test_occ_to_streamer_symbol(): + dxf = '.SPY240324P480.5' + occ = 'SPY 240324P00480500' + assert Option.occ_to_streamer_symbol(occ) == dxf