From a3ac17777512a57e582379d3397592ceb9348491 Mon Sep 17 00:00:00 2001 From: Graeme Holliday Date: Thu, 10 Oct 2024 21:28:59 -0500 Subject: [PATCH] add order chains, add is_market_open_on util (#171) --- tastytrade/__init__.py | 1 + tastytrade/account.py | 86 +++++++++++++++++++++++++++++++++++++++++- tastytrade/order.py | 6 +-- tastytrade/utils.py | 13 +++++++ tests/test_account.py | 15 ++++++++ tests/test_streamer.py | 3 +- 6 files changed, 118 insertions(+), 6 deletions(-) diff --git a/tastytrade/__init__.py b/tastytrade/__init__.py index 3f27b0c..2398656 100644 --- a/tastytrade/__init__.py +++ b/tastytrade/__init__.py @@ -3,6 +3,7 @@ API_URL = "https://api.tastyworks.com" BACKTEST_URL = "https://backtester.vast.tastyworks.com" CERT_URL = "https://api.cert.tastyworks.com" +VAST_URL = "https://vast.tastyworks.com" VERSION = "9.0" logger = logging.getLogger(__name__) diff --git a/tastytrade/account.py b/tastytrade/account.py index 3a2c5e6..b665948 100644 --- a/tastytrade/account.py +++ b/tastytrade/account.py @@ -2,14 +2,17 @@ from decimal import Decimal from typing import Any, Dict, List, Literal, Optional, Union +import httpx from pydantic import BaseModel, model_validator from typing_extensions import Self +from tastytrade import VAST_URL from tastytrade.order import ( InstrumentType, NewComplexOrder, NewOrder, OrderAction, + OrderChain, OrderStatus, PlacedComplexOrder, PlacedComplexOrderResponse, @@ -26,6 +29,8 @@ validate_response, ) +TT_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ" + class EmptyDict(BaseModel): class Config: @@ -1049,7 +1054,7 @@ async def a_get_net_liquidating_value_history( params = {} if start_time: # format to Tastytrade DateTime format - params = {"start-time": start_time.strftime("%Y-%m-%dT%H:%M:%SZ")} + params = {"start-time": start_time.strftime(TT_DATE_FMT)} elif not time_back: msg = "Either time_back or start_time must be specified." raise TastytradeError(msg) @@ -1083,7 +1088,7 @@ def get_net_liquidating_value_history( params = {} if start_time: # format to Tastytrade DateTime format - params = {"start-time": start_time.strftime("%Y-%m-%dT%H:%M:%SZ")} + params = {"start-time": start_time.strftime(TT_DATE_FMT)} elif not time_back: msg = "Either time_back or start_time must be specified." raise TastytradeError(msg) @@ -1641,3 +1646,80 @@ def replace_order( ), ) return PlacedOrder(**data) + + async def a_get_order_chains( + self, + session: Session, + symbol: str, + start_time: datetime, + end_time: datetime, + ) -> List[OrderChain]: + """ + Get a list of order chains (open + rolls + close) for given symbol + over the given time frame, with total P/L, commissions, etc. + + :param session: the session to use for the request. + :param symbol: the underlying symbol for the chains. + :param start_time: the beginning time of the query. + :param end_time: the ending time of the query. + """ + params = { + "account-numbers[]": self.account_number, + "underlying-symbols[]": symbol, + "start-at": start_time.strftime(TT_DATE_FMT), + "end-at": end_time.strftime(TT_DATE_FMT), + "defer-open-winner-loser-filtering-to-frontend": False, + "per-page": 250, + } + headers = { + "Authorization": session.session_token, + "Accept": "application/json", + "Content-Type": "application/json", + } + async with httpx.AsyncClient() as client: + response = await client.get( + f"{VAST_URL}/order-chains", + headers=headers, + params=params, + ) + validate_response(response) + chains = response.json()["data"]["items"] + return [OrderChain(**i) for i in chains] + + def get_order_chains( + self, + session: Session, + symbol: str, + start_time: datetime, + end_time: datetime, + ) -> List[OrderChain]: + """ + Get a list of order chains (open + rolls + close) for given symbol + over the given time frame, with total P/L, commissions, etc. + + :param session: the session to use for the request. + :param symbol: the underlying symbol for the chains. + :param start_time: the beginning time of the query. + :param end_time: the ending time of the query. + """ + params = { + "account-numbers[]": self.account_number, + "underlying-symbols[]": symbol, + "start-at": start_time.strftime(TT_DATE_FMT), + "end-at": end_time.strftime(TT_DATE_FMT), + "defer-open-winner-loser-filtering-to-frontend": False, + "per-page": 250, + } + headers = { + "Authorization": session.session_token, + "Accept": "application/json", + "Content-Type": "application/json", + } + response = httpx.get( + f"{VAST_URL}/order-chains", + headers=headers, + params=params, + ) + validate_response(response) + chains = response.json()["data"]["items"] + return [OrderChain(**i) for i in chains] diff --git a/tastytrade/order.py b/tastytrade/order.py index 04a1bb7..44ecd0a 100644 --- a/tastytrade/order.py +++ b/tastytrade/order.py @@ -528,11 +528,11 @@ class OrderChain(TastytradeJsonDataclass): """ id: int - updated_at: datetime - created_at: datetime account_number: str description: str underlying_symbol: str computed_data: ComputedData - lite_nodes_sizes: int lite_nodes: List[OrderChainNode] + lite_nodes_sizes: Optional[int] = None + updated_at: Optional[datetime] = None + created_at: Optional[datetime] = None diff --git a/tastytrade/utils.py b/tastytrade/utils.py index f3c6bb1..02286b5 100644 --- a/tastytrade/utils.py +++ b/tastytrade/utils.py @@ -41,6 +41,19 @@ def today_in_new_york() -> date: return now_in_new_york().date() +def is_market_open_on(day: date = today_in_new_york()) -> bool: + """ + Returns whether the market was/is/will be open at ANY point + during the given day. + + :param day: date to check + + :return: whether the market opens on given day + """ + date_range = NYSE.valid_days(day, day) + return len(date_range) != 0 + + def get_third_friday(day: date = today_in_new_york()) -> date: """ Gets the monthly expiration associated with the month of the given date, diff --git a/tests/test_account.py b/tests/test_account.py index 6bfc48b..a1312ff 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -1,4 +1,5 @@ import os +from datetime import datetime from decimal import Decimal from time import sleep @@ -148,6 +149,20 @@ async def test_get_live_orders_async(session, account): await account.a_get_live_orders(session) +def test_get_order_chains(session, account): + start_time = datetime(2024, 1, 1, 0, 0, 0) + end_time = datetime.now() + account.get_order_chains(session, "F", start_time=start_time, end_time=end_time) + + +async def test_get_order_chains_async(session, account): + start_time = datetime(2024, 1, 1, 0, 0, 0) + end_time = datetime.now() + await account.a_get_order_chains( + session, "F", start_time=start_time, end_time=end_time + ) + + @fixture(scope="module") def new_order(session): symbol = Equity.get_equity(session, "F") diff --git a/tests/test_streamer.py b/tests/test_streamer.py index f5505ce..fb7747e 100644 --- a/tests/test_streamer.py +++ b/tests/test_streamer.py @@ -27,4 +27,5 @@ async def test_dxlink_streamer(session): async for _ in streamer.listen(Quote): break await streamer.unsubscribe_candle(subs[0], "1d") - await streamer.unsubscribe(Quote, subs) + await streamer.unsubscribe(Quote, [subs[0]]) + await streamer.unsubscribe_all(Quote)