diff --git a/docs/index.rst b/docs/index.rst index 293bb63..bc0c43e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,7 @@ A simple, reverse-engineered, sync/async SDK for Tastytrade built on their (now account-streamer data-streamer backtest + market-sessions watchlists .. toctree:: diff --git a/docs/market-sessions.rst b/docs/market-sessions.rst new file mode 100644 index 0000000..9ac5692 --- /dev/null +++ b/docs/market-sessions.rst @@ -0,0 +1,38 @@ +Market Sessions +=============== + +A market time session object contains information about the current state of specific markets. It can be used to get the market opening and closing times and state. + +The dataclass represents the current session and any nested 'next' or 'previous' session info. + +The ``get_market_sessions`` function can be used to obtain information about the current session: + +.. code-block:: python + + from tastytrade.market_sessions import ExchangeType, get_market_sessions + get_market_sessions(session, exchanges=[ExchangeType.NYSE]) + +>>> [MarketSession(close_at=None, close_at_ext=None, instrument_collection='Equity', open_at=None, start_at=None, next_session=MarketSessionSnapshot(close_at=datetime.datetime(2025, 2, 18, 21, 0, tzinfo=TzInfo(UTC)), close_at_ext=datetime.datetime(2025, 2, 19, 1, 0, tzinfo=TzInfo(UTC)), instrument_collection='Equity', open_at=datetime.datetime(2025, 2, 18, 14, 30, tzinfo=TzInfo(UTC)), session_date=datetime.date(2025, 2, 18), start_at=datetime.datetime(2025, 2, 18, 13, 15, tzinfo=TzInfo(UTC))), previous_session=MarketSessionSnapshot(close_at=datetime.datetime(2025, 2, 14, 21, 0, tzinfo=TzInfo(UTC)), close_at_ext=datetime.datetime(2025, 2, 15, 1, 0, tzinfo=TzInfo(UTC)), instrument_collection='Equity', open_at=datetime.datetime(2025, 2, 14, 14, 30, tzinfo=TzInfo(UTC)), session_date=datetime.date(2025, 2, 14), start_at=datetime.datetime(2025, 2, 14, 13, 15, tzinfo=TzInfo(UTC))), status=)] + +The ``get_market_holidays`` function can be used to obtain information about markets half days and holidays: + +.. code-block:: python + + from tastytrade.market_sessions import get_market_holidays + calendar = Market.get_market_holidays(session) + print(calendar.half_days) + print(calendar.holidays) + +>>> [datetime.date(2015, 12, 24), datetime.date(2016, 11, 25), datetime.date(2017, 7, 3), datetime.date(2017, 11, 24), datetime.date(2018, 7, 3), datetime.date(2018, 11, 23), datetime.date(2018, 12, 24), datetime.date(2019, 7, 3), datetime.date(2019, 11, 29), datetime.date(2019, 12, 24), datetime.date(2020, 11, 27), datetime.date(2020, 12, 24), datetime.date(2021, 11, 26), datetime.date(2022, 11, 25), datetime.date(2023, 7, 3), datetime.date(2023, 11, 24), datetime.date(2024, 7, 3), datetime.date(2024, 11, 29), datetime.date(2024, 12, 24), datetime.date(2025, 7, 3), datetime.date(2025, 11, 28), datetime.date(2025, 12, 24), datetime.date(2026, 11, 27), datetime.date(2026, 12, 24), datetime.date(2027, 7, 2), datetime.date(2027, 11, 26), datetime.date(2027, 12, 23), datetime.date(2028, 7, 3), datetime.date(2028, 11, 24), datetime.date(2028, 12, 22), datetime.date(2029, 7, 3)] +>>> [datetime.date(2015, 12, 25), datetime.date(2016, 1, 1), datetime.date(2016, 1, 18), datetime.date(2016, 2, 15), datetime.date(2016, 3, 25), datetime.date(2016, 5, 30), datetime.date(2016, 7, 4), datetime.date(2016, 9, 5), datetime.date(2016, 11, 24), datetime.date(2016, 12, 26), datetime.date(2017, 1, 2), datetime.date(2017, 1, 16), datetime.date(2017, 2, 20), datetime.date(2017, 4, 14), datetime.date(2017, 5, 29), datetime.date(2017, 7, 4), datetime.date(2017, 9, 4), datetime.date(2017, 11, 23), datetime.date(2017, 12, 25), datetime.date(2018, 1, 1), datetime.date(2018, 1, 15), datetime.date(2018, 2, 19), datetime.date(2018, 3, 30), datetime.date(2018, 5, 28), datetime.date(2018, 7, 4), datetime.date(2018, 9, 3), datetime.date(2018, 11, 22), datetime.date(2018, 12, 5), datetime.date(2018, 12, 25), datetime.date(2019, 1, 1), datetime.date(2019, 1, 21), datetime.date(2019, 2, 18), datetime.date(2019, 4, 19), datetime.date(2019, 5, 27), datetime.date(2019, 7, 4), datetime.date(2019, 9, 2), datetime.date(2019, 11, 28), datetime.date(2019, 12, 25), datetime.date(2020, 1, 1), datetime.date(2020, 1, 20), datetime.date(2020, 2, 17), datetime.date(2020, 4, 10), datetime.date(2020, 5, 25), datetime.date(2020, 7, 3), datetime.date(2020, 9, 7), datetime.date(2020, 11, 26), datetime.date(2020, 12, 25), datetime.date(2021, 1, 1), datetime.date(2021, 1, 18), datetime.date(2021, 2, 15), datetime.date(2021, 4, 2), datetime.date(2021, 5, 31), datetime.date(2021, 7, 5), datetime.date(2021, 9, 6), datetime.date(2021, 11, 25), datetime.date(2021, 12, 24), datetime.date(2022, 1, 17), datetime.date(2022, 2, 21), datetime.date(2022, 4, 15), datetime.date(2022, 5, 30), datetime.date(2022, 6, 20), datetime.date(2022, 7, 4), datetime.date(2022, 9, 5), datetime.date(2022, 11, 24), datetime.date(2022, 12, 26), datetime.date(2023, 1, 2), datetime.date(2023, 1, 16), datetime.date(2023, 2, 20), datetime.date(2023, 4, 7), datetime.date(2023, 5, 29), datetime.date(2023, 6, 19), datetime.date(2023, 7, 4), datetime.date(2023, 9, 4), datetime.date(2023, 11, 23), datetime.date(2023, 12, 25), datetime.date(2024, 1, 1), datetime.date(2024, 1, 15), datetime.date(2024, 2, 19), datetime.date(2024, 3, 29), datetime.date(2024, 5, 27), datetime.date(2024, 6, 19), datetime.date(2024, 7, 4), datetime.date(2024, 9, 2), datetime.date(2024, 11, 28), datetime.date(2024, 12, 25), datetime.date(2025, 1, 1), datetime.date(2025, 1, 9), datetime.date(2025, 1, 20), datetime.date(2025, 2, 17), datetime.date(2025, 4, 18), datetime.date(2025, 5, 26), datetime.date(2025, 6, 19), datetime.date(2025, 7, 4), datetime.date(2025, 9, 1), datetime.date(2025, 11, 27), datetime.date(2025, 12, 25), datetime.date(2026, 1, 1), datetime.date(2026, 1, 19), datetime.date(2026, 2, 16), datetime.date(2026, 4, 3), datetime.date(2026, 5, 25), datetime.date(2026, 6, 19), datetime.date(2026, 7, 3), datetime.date(2026, 9, 7), datetime.date(2026, 11, 26), datetime.date(2026, 12, 25), datetime.date(2027, 1, 1), datetime.date(2027, 1, 18), datetime.date(2027, 2, 15), datetime.date(2027, 3, 26), datetime.date(2027, 5, 31), datetime.date(2027, 6, 18), datetime.date(2027, 7, 5), datetime.date(2027, 9, 6), datetime.date(2027, 11, 25), datetime.date(2027, 12, 24), datetime.date(2028, 1, 17), datetime.date(2028, 2, 21), datetime.date(2028, 4, 14), datetime.date(2028, 5, 29), datetime.date(2028, 6, 19), datetime.date(2028, 7, 4), datetime.date(2028, 9, 4), datetime.date(2028, 11, 23), datetime.date(2028, 12, 25), datetime.date(2029, 1, 1), datetime.date(2029, 1, 15), datetime.date(2029, 2, 19), datetime.date(2029, 3, 30), datetime.date(2029, 5, 28), datetime.date(2029, 6, 19), datetime.date(2029, 7, 4), datetime.date(2029, 9, 3)] + +I case you only want to extract the market status, this is one way to do it: + +.. code-block:: python + + from tastytrade.market_sessions import ExchangeType, MarketStatus, get_market_sessions + + market_sessions = get_market_sessions(session, exchanges=[ExchangeType.NYSE, ExchangeType.CME]) + print([ms.status != MarketStatus.CLOSED for ms in market_sessions]) + +>>> [False, False] diff --git a/tastytrade/market_sessions.py b/tastytrade/market_sessions.py new file mode 100644 index 0000000..941fc91 --- /dev/null +++ b/tastytrade/market_sessions.py @@ -0,0 +1,119 @@ +from datetime import date, datetime +from enum import Enum +from typing import Optional + +from pydantic import Field + +from tastytrade.session import Session +from tastytrade.utils import TastytradeJsonDataclass + + +class ExchangeType(str, Enum): + """ + Contains the valid exchanges to get market sessions for. + """ + + CME = "CME" + CFE = "CFE" + NYSE = "Equity" + SMALL = "Smalls" + + +class MarketStatus(str, Enum): + """ + Contains the valid market status values. + """ + + OPEN = "Open" + CLOSED = "Closed" + PRE_MARKET = "Pre-market" + EXTENDED = "Extended" + + +class MarketSessionSnapshot(TastytradeJsonDataclass): + """ + Dataclass containing information about the upcoming or previous market session. + """ + + close_at: datetime + close_at_ext: Optional[datetime] = None + instrument_collection: str + open_at: datetime + session_date: date + start_at: datetime + + +class MarketSession(TastytradeJsonDataclass): + """ + Dataclass representing the current session as well as the next and previous sessions. + """ + + close_at: Optional[datetime] = None + close_at_ext: Optional[datetime] = None + instrument_collection: str + open_at: Optional[datetime] = None + start_at: Optional[datetime] = None + next_session: Optional[MarketSessionSnapshot] = None + previous_session: Optional[MarketSessionSnapshot] = None + status: MarketStatus = Field(alias="state") + + +class MarketCalendar(TastytradeJsonDataclass): + """ + Dataclass containing information about market holidays and shortened days. + """ + + half_days: list[date] = Field(alias="market-half-days") + holidays: list[date] = Field(alias="market-holidays") + + +async def a_get_market_sessions( + session: Session, exchanges: list[ExchangeType] +) -> list[MarketSession]: + """ + Retrieves a list of session timings for the given exchanges. + + :param session: active user session to use + :param exchanges: the list of exchanges to get market sessions for + """ + data = await session._a_get( + "/market-time/sessions/current", + params={"instrument-collections[]": [e.value for e in exchanges]}, + ) + return [MarketSession(**i) for i in data["items"]] + + +def get_market_sessions( + session: Session, exchanges: list[ExchangeType] +) -> list[MarketSession]: + """ + Retrieves a list of session timings for the given exchanges. + + :param session: active user session to use + :param exchanges: the list of exchanges to get market sessions for + """ + data = session._get( + "/market-time/sessions/current", + params={"instrument-collections[]": [e.value for e in exchanges]}, + ) + return [MarketSession(**i) for i in data["items"]] + + +async def a_get_market_holidays(session: Session) -> MarketCalendar: + """ + Retrieves market calendar for half days and holidays. + + :param session: active user session to use + """ + data = await session._a_get("/market-time/equities/holidays") + return MarketCalendar(**data) + + +def get_market_holidays(session: Session) -> MarketCalendar: + """ + Retrieves market calendar for half days and holidays. + + :param session: active user session to use + """ + data = session._get("/market-time/equities/holidays") + return MarketCalendar(**data) diff --git a/tests/test_market_sessions.py b/tests/test_market_sessions.py new file mode 100644 index 0000000..25c8cd2 --- /dev/null +++ b/tests/test_market_sessions.py @@ -0,0 +1,33 @@ +from pytest import fixture + +from tastytrade import Session +from tastytrade.market_sessions import ( + ExchangeType, + a_get_market_sessions, + a_get_market_holidays, + get_market_sessions, + get_market_holidays, +) + + +@fixture +def exchanges() -> list[ExchangeType]: + return [ExchangeType.NYSE, ExchangeType.CME, ExchangeType.CFE, ExchangeType.SMALL] + + +async def test_get_market_sessions_async( + session: Session, exchanges: list[ExchangeType] +): + await a_get_market_sessions(session, exchanges=exchanges) + + +async def test_get_market_holidays_async(session: Session): + await a_get_market_holidays(session) + + +def test_get_market_sessions(session: Session, exchanges: list[ExchangeType]): + get_market_sessions(session, exchanges=exchanges) + + +def test_get_market_holidays(session: Session): + get_market_holidays(session)