tastytrade 11.0.5__tar.gz → 11.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tastytrade-11.0.5 → tastytrade-11.1.0}/PKG-INFO +1 -1
- {tastytrade-11.0.5 → tastytrade-11.1.0}/pyproject.toml +1 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/__init__.py +1 -2
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/account.py +0 -171
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/market_sessions.py +19 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/order.py +3 -120
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/session.py +5 -12
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/streamer.py +2 -9
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/utils.py +17 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_account.py +111 -54
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_dxfeed.py +10 -17
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_market_sessions.py +10 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_utils.py +69 -1
- {tastytrade-11.0.5 → tastytrade-11.1.0}/uv.lock +48 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/.github/CONTRIBUTING.md +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/.github/FUNDING.yml +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/.github/pull_request_template.md +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/.github/workflows/python-app.yml +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/.github/workflows/python-publish-test.yml +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/.github/workflows/python-publish.yml +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/.gitignore +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/.python-version +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/.readthedocs.yaml +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/LICENSE +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/Makefile +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/README.md +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/Makefile +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/account-streamer.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/accounts.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/account.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/dxfeed.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/instruments.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/market-data.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/market-sessions.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/metrics.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/order.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/search.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/session.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/streamer.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/utils.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/api/watchlists.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/conf.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/data-streamer.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/img/netliq.png +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/index.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/installation.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/instruments.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/make.bat +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/market-data.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/market-sessions.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/orders.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/sessions.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/sync-async.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/docs/watchlists.rst +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/__init__.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/candle.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/event.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/greeks.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/profile.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/quote.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/summary.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/theoprice.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/timeandsale.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/trade.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/dxfeed/underlying.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/instruments.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/market_data.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/metrics.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/py.typed +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/search.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tastytrade/watchlists.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/__init__.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/conftest.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_instruments.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_market_data.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_metrics.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_search.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_session.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_streamer.py +0 -0
- {tastytrade-11.0.5 → tastytrade-11.1.0}/tests/test_watchlists.py +0 -0
|
@@ -3,8 +3,7 @@ import logging
|
|
|
3
3
|
API_URL = "https://api.tastyworks.com"
|
|
4
4
|
API_VERSION = "20251101"
|
|
5
5
|
CERT_URL = "https://api.cert.tastyworks.com"
|
|
6
|
-
|
|
7
|
-
VERSION = "11.0.5"
|
|
6
|
+
VERSION = "11.1.0"
|
|
8
7
|
|
|
9
8
|
__version__ = VERSION
|
|
10
9
|
version_str: str = f"tastyware/tastytrade:v{VERSION}"
|
|
@@ -2,17 +2,14 @@ from datetime import date, datetime
|
|
|
2
2
|
from decimal import Decimal
|
|
3
3
|
from typing import Any, Literal, cast, overload
|
|
4
4
|
|
|
5
|
-
import httpx
|
|
6
5
|
from pydantic import BaseModel, ConfigDict, model_validator
|
|
7
6
|
from typing_extensions import Self
|
|
8
7
|
|
|
9
|
-
from tastytrade import VAST_URL
|
|
10
8
|
from tastytrade.order import (
|
|
11
9
|
InstrumentType,
|
|
12
10
|
NewComplexOrder,
|
|
13
11
|
NewOrder,
|
|
14
12
|
OrderAction,
|
|
15
|
-
OrderChain,
|
|
16
13
|
OrderStatus,
|
|
17
14
|
PlacedComplexOrder,
|
|
18
15
|
PlacedComplexOrderResponse,
|
|
@@ -28,7 +25,6 @@ from tastytrade.utils import (
|
|
|
28
25
|
paginate,
|
|
29
26
|
set_sign_for,
|
|
30
27
|
today_in_new_york,
|
|
31
|
-
validate_response,
|
|
32
28
|
)
|
|
33
29
|
|
|
34
30
|
TT_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ"
|
|
@@ -296,23 +292,6 @@ class MarginReport(TastytradeData):
|
|
|
296
292
|
)
|
|
297
293
|
|
|
298
294
|
|
|
299
|
-
class MarginRequirement(TastytradeData):
|
|
300
|
-
"""
|
|
301
|
-
Dataclass containing general margin requirement information for a symbol.
|
|
302
|
-
"""
|
|
303
|
-
|
|
304
|
-
underlying_symbol: str
|
|
305
|
-
long_equity_initial: Decimal
|
|
306
|
-
short_equity_initial: Decimal
|
|
307
|
-
long_equity_maintenance: Decimal
|
|
308
|
-
short_equity_maintenance: Decimal
|
|
309
|
-
naked_option_standard: Decimal
|
|
310
|
-
naked_option_minimum: Decimal
|
|
311
|
-
naked_option_floor: Decimal
|
|
312
|
-
clearing_identifier: str | None = None
|
|
313
|
-
is_deleted: bool | None = None
|
|
314
|
-
|
|
315
|
-
|
|
316
295
|
class NetLiqOhlc(TastytradeData):
|
|
317
296
|
"""
|
|
318
297
|
Dataclass containing historical net liquidation data in OHLC format
|
|
@@ -334,23 +313,6 @@ class NetLiqOhlc(TastytradeData):
|
|
|
334
313
|
time: str
|
|
335
314
|
|
|
336
315
|
|
|
337
|
-
class PositionLimit(TastytradeData):
|
|
338
|
-
"""
|
|
339
|
-
Dataclass containing information about general account limits.
|
|
340
|
-
"""
|
|
341
|
-
|
|
342
|
-
account_number: str
|
|
343
|
-
equity_order_size: int
|
|
344
|
-
equity_option_order_size: int
|
|
345
|
-
future_order_size: int
|
|
346
|
-
future_option_order_size: int
|
|
347
|
-
underlying_opening_order_limit: int
|
|
348
|
-
equity_position_size: int
|
|
349
|
-
equity_option_position_size: int
|
|
350
|
-
future_position_size: int
|
|
351
|
-
future_option_position_size: int
|
|
352
|
-
|
|
353
|
-
|
|
354
316
|
class TradingStatus(TastytradeData):
|
|
355
317
|
"""
|
|
356
318
|
Dataclass containing information about an account's trading status, such
|
|
@@ -1043,58 +1005,6 @@ class Account(TastytradeData):
|
|
|
1043
1005
|
)
|
|
1044
1006
|
return [NetLiqOhlc(**i) for i in data["items"]]
|
|
1045
1007
|
|
|
1046
|
-
async def a_get_position_limit(self, session: Session) -> PositionLimit:
|
|
1047
|
-
"""
|
|
1048
|
-
Get the maximum order size information for the account.
|
|
1049
|
-
|
|
1050
|
-
:param session: the session to use for the request.
|
|
1051
|
-
"""
|
|
1052
|
-
data = await session._a_get(f"/accounts/{self.account_number}/position-limit")
|
|
1053
|
-
return PositionLimit(**data)
|
|
1054
|
-
|
|
1055
|
-
def get_position_limit(self, session: Session) -> PositionLimit:
|
|
1056
|
-
"""
|
|
1057
|
-
Get the maximum order size information for the account.
|
|
1058
|
-
|
|
1059
|
-
:param session: the session to use for the request.
|
|
1060
|
-
"""
|
|
1061
|
-
data = session._get(f"/accounts/{self.account_number}/position-limit")
|
|
1062
|
-
return PositionLimit(**data)
|
|
1063
|
-
|
|
1064
|
-
async def a_get_effective_margin_requirements(
|
|
1065
|
-
self, session: Session, symbol: str
|
|
1066
|
-
) -> MarginRequirement:
|
|
1067
|
-
"""
|
|
1068
|
-
Get the effective margin requirements for a given symbol.
|
|
1069
|
-
|
|
1070
|
-
:param session:
|
|
1071
|
-
the session to use for the request, can't be certification
|
|
1072
|
-
:param symbol: the symbol to get margin requirements for.
|
|
1073
|
-
"""
|
|
1074
|
-
if symbol:
|
|
1075
|
-
symbol = symbol.replace("/", "%2F")
|
|
1076
|
-
data = await session._a_get(
|
|
1077
|
-
f"/accounts/{self.account_number}/margin-requirements/{symbol}/effective"
|
|
1078
|
-
)
|
|
1079
|
-
return MarginRequirement(**data)
|
|
1080
|
-
|
|
1081
|
-
def get_effective_margin_requirements(
|
|
1082
|
-
self, session: Session, symbol: str
|
|
1083
|
-
) -> MarginRequirement:
|
|
1084
|
-
"""
|
|
1085
|
-
Get the effective margin requirements for a given symbol.
|
|
1086
|
-
|
|
1087
|
-
:param session:
|
|
1088
|
-
the session to use for the request, can't be certification
|
|
1089
|
-
:param symbol: the symbol to get margin requirements for.
|
|
1090
|
-
"""
|
|
1091
|
-
if symbol:
|
|
1092
|
-
symbol = symbol.replace("/", "%2F")
|
|
1093
|
-
data = session._get(
|
|
1094
|
-
f"/accounts/{self.account_number}/margin-requirements/{symbol}/effective"
|
|
1095
|
-
)
|
|
1096
|
-
return MarginRequirement(**data)
|
|
1097
|
-
|
|
1098
1008
|
async def a_get_margin_requirements(self, session: Session) -> MarginReport:
|
|
1099
1009
|
"""
|
|
1100
1010
|
Get the margin report for the account, with total margin requirements
|
|
@@ -1500,84 +1410,3 @@ class Account(TastytradeData):
|
|
|
1500
1410
|
),
|
|
1501
1411
|
)
|
|
1502
1412
|
return PlacedOrder(**data)
|
|
1503
|
-
|
|
1504
|
-
async def a_get_order_chains(
|
|
1505
|
-
self,
|
|
1506
|
-
session: Session,
|
|
1507
|
-
symbol: str,
|
|
1508
|
-
start_time: datetime,
|
|
1509
|
-
end_time: datetime,
|
|
1510
|
-
) -> list[OrderChain]:
|
|
1511
|
-
"""
|
|
1512
|
-
Get a list of order chains (open + rolls + close) for given symbol
|
|
1513
|
-
over the given time frame, with total P/L, commissions, etc.
|
|
1514
|
-
|
|
1515
|
-
Not supported for OAuth sessions--write Tasty to get this added!
|
|
1516
|
-
|
|
1517
|
-
:param session: the session to use for the request.
|
|
1518
|
-
:param symbol: the underlying symbol for the chains.
|
|
1519
|
-
:param start_time: the beginning time of the query.
|
|
1520
|
-
:param end_time: the ending time of the query.
|
|
1521
|
-
"""
|
|
1522
|
-
params = {
|
|
1523
|
-
"account-numbers[]": self.account_number,
|
|
1524
|
-
"underlying-symbols[]": symbol,
|
|
1525
|
-
"start-at": start_time.strftime(TT_DATE_FMT),
|
|
1526
|
-
"end-at": end_time.strftime(TT_DATE_FMT),
|
|
1527
|
-
"defer-open-winner-loser-filtering-to-frontend": False,
|
|
1528
|
-
"per-page": 250,
|
|
1529
|
-
}
|
|
1530
|
-
headers = {
|
|
1531
|
-
"Authorization": session.session_token,
|
|
1532
|
-
"Accept": "application/json",
|
|
1533
|
-
"Content-Type": "application/json",
|
|
1534
|
-
}
|
|
1535
|
-
async with httpx.AsyncClient() as client:
|
|
1536
|
-
response = await client.get(
|
|
1537
|
-
f"{VAST_URL}/order-chains",
|
|
1538
|
-
headers=headers,
|
|
1539
|
-
params=params, # type: ignore[arg-type]
|
|
1540
|
-
)
|
|
1541
|
-
validate_response(response)
|
|
1542
|
-
chains = response.json()["data"]["items"]
|
|
1543
|
-
return [OrderChain(**i) for i in chains]
|
|
1544
|
-
|
|
1545
|
-
def get_order_chains(
|
|
1546
|
-
self,
|
|
1547
|
-
session: Session,
|
|
1548
|
-
symbol: str,
|
|
1549
|
-
start_time: datetime,
|
|
1550
|
-
end_time: datetime,
|
|
1551
|
-
) -> list[OrderChain]:
|
|
1552
|
-
"""
|
|
1553
|
-
Get a list of order chains (open + rolls + close) for given symbol
|
|
1554
|
-
over the given time frame, with total P/L, commissions, etc.
|
|
1555
|
-
|
|
1556
|
-
Not supported for OAuth sessions--write Tasty to get this added!
|
|
1557
|
-
|
|
1558
|
-
:param session: the session to use for the request.
|
|
1559
|
-
:param symbol: the underlying symbol for the chains.
|
|
1560
|
-
:param start_time: the beginning time of the query.
|
|
1561
|
-
:param end_time: the ending time of the query.
|
|
1562
|
-
"""
|
|
1563
|
-
params = {
|
|
1564
|
-
"account-numbers[]": self.account_number,
|
|
1565
|
-
"underlying-symbols[]": symbol,
|
|
1566
|
-
"start-at": start_time.strftime(TT_DATE_FMT),
|
|
1567
|
-
"end-at": end_time.strftime(TT_DATE_FMT),
|
|
1568
|
-
"defer-open-winner-loser-filtering-to-frontend": False,
|
|
1569
|
-
"per-page": 250,
|
|
1570
|
-
}
|
|
1571
|
-
headers = {
|
|
1572
|
-
"Authorization": session.session_token,
|
|
1573
|
-
"Accept": "application/json",
|
|
1574
|
-
"Content-Type": "application/json",
|
|
1575
|
-
}
|
|
1576
|
-
response = httpx.get(
|
|
1577
|
-
f"{VAST_URL}/order-chains",
|
|
1578
|
-
headers=headers,
|
|
1579
|
-
params=params, # type: ignore[arg-type]
|
|
1580
|
-
)
|
|
1581
|
-
validate_response(response)
|
|
1582
|
-
chains = response.json()["data"]["items"]
|
|
1583
|
-
return [OrderChain(**i) for i in chains]
|
|
@@ -118,6 +118,25 @@ def get_market_holidays(session: Session) -> MarketCalendar:
|
|
|
118
118
|
return MarketCalendar(**data)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
async def a_get_futures_holidays(
|
|
122
|
+
session: Session, exchange: ExchangeType
|
|
123
|
+
) -> MarketCalendar:
|
|
124
|
+
"""
|
|
125
|
+
Retrieves market calendar for half days and holidays for a futures exchange.
|
|
126
|
+
|
|
127
|
+
:param session: active user session to use
|
|
128
|
+
:param exchange: exchange to fetch calendar for
|
|
129
|
+
"""
|
|
130
|
+
data = await session._a_get(f"/market-time/futures/holidays/{exchange.value}")
|
|
131
|
+
return MarketCalendar(**data)
|
|
132
|
+
|
|
133
|
+
|
|
121
134
|
def get_futures_holidays(session: Session, exchange: ExchangeType) -> MarketCalendar:
|
|
135
|
+
"""
|
|
136
|
+
Retrieves market calendar for half days and holidays for a futures exchange.
|
|
137
|
+
|
|
138
|
+
:param session: active user session to use
|
|
139
|
+
:param exchange: exchange to fetch calendar for
|
|
140
|
+
"""
|
|
122
141
|
data = session._get(f"/market-time/futures/holidays/{exchange.value}")
|
|
123
142
|
return MarketCalendar(**data)
|
|
@@ -3,7 +3,7 @@ from decimal import Decimal
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from pydantic import computed_field, field_serializer, model_validator
|
|
6
|
+
from pydantic import ConfigDict, computed_field, field_serializer, model_validator
|
|
7
7
|
|
|
8
8
|
from tastytrade import version_str
|
|
9
9
|
from tastytrade.utils import (
|
|
@@ -237,6 +237,8 @@ class NewOrder(TastytradeData):
|
|
|
237
237
|
modifying existing orders.
|
|
238
238
|
"""
|
|
239
239
|
|
|
240
|
+
model_config = ConfigDict(extra="allow")
|
|
241
|
+
|
|
240
242
|
time_in_force: OrderTimeInForce
|
|
241
243
|
order_type: OrderType
|
|
242
244
|
source: str = version_str
|
|
@@ -435,122 +437,3 @@ class PlacedOrderResponse(TastytradeData):
|
|
|
435
437
|
fee_calculation: FeeCalculation | None = None
|
|
436
438
|
warnings: list[Message] | None = None
|
|
437
439
|
errors: list[Message] | None = None
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
class OrderChainEntry(TastytradeData):
|
|
441
|
-
"""
|
|
442
|
-
Dataclass containing information about a single order in an order chain.
|
|
443
|
-
"""
|
|
444
|
-
|
|
445
|
-
symbol: str
|
|
446
|
-
instrument_type: InstrumentType
|
|
447
|
-
quantity: str
|
|
448
|
-
quantity_type: str
|
|
449
|
-
quantity_numeric: Decimal
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
class OrderChainLeg(TastytradeData):
|
|
453
|
-
"""
|
|
454
|
-
Dataclass containing information about a single leg in an order
|
|
455
|
-
from an order chain.
|
|
456
|
-
"""
|
|
457
|
-
|
|
458
|
-
symbol: str
|
|
459
|
-
instrument_type: InstrumentType
|
|
460
|
-
action: OrderAction
|
|
461
|
-
fill_quantity: Decimal
|
|
462
|
-
order_quantity: Decimal
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
class OrderChainNode(TastytradeData):
|
|
466
|
-
"""
|
|
467
|
-
Dataclass containing information about a single node in an order chain.
|
|
468
|
-
"""
|
|
469
|
-
|
|
470
|
-
node_type: str
|
|
471
|
-
id: str
|
|
472
|
-
description: str
|
|
473
|
-
occurred_at: datetime | None = None
|
|
474
|
-
total_fees: Decimal | None = None
|
|
475
|
-
total_fill_cost: Decimal | None = None
|
|
476
|
-
gcd_quantity: Decimal | None = None
|
|
477
|
-
fill_cost_per_quantity: Decimal | None = None
|
|
478
|
-
order_fill_count: int | None = None
|
|
479
|
-
roll: bool | None = None
|
|
480
|
-
legs: list[OrderChainLeg] | None = None
|
|
481
|
-
entries: list[OrderChainEntry] | None = None
|
|
482
|
-
|
|
483
|
-
@model_validator(mode="before")
|
|
484
|
-
@classmethod
|
|
485
|
-
def validate_price_effects(cls, data: Any) -> Any:
|
|
486
|
-
return set_sign_for(
|
|
487
|
-
data,
|
|
488
|
-
[
|
|
489
|
-
"total_fees",
|
|
490
|
-
"total_fill_cost",
|
|
491
|
-
"fill_cost_per_quantity",
|
|
492
|
-
],
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
class ComputedData(TastytradeData):
|
|
497
|
-
"""
|
|
498
|
-
Dataclass containing computed data about an order chain.
|
|
499
|
-
"""
|
|
500
|
-
|
|
501
|
-
open: bool
|
|
502
|
-
updated_at: datetime
|
|
503
|
-
total_fees: Decimal
|
|
504
|
-
total_commissions: Decimal
|
|
505
|
-
realized_gain: Decimal
|
|
506
|
-
realized_gain_with_fees: Decimal
|
|
507
|
-
winner_realized_and_closed: bool
|
|
508
|
-
winner_realized: bool
|
|
509
|
-
winner_realized_with_fees: bool
|
|
510
|
-
roll_count: int
|
|
511
|
-
opened_at: datetime
|
|
512
|
-
last_occurred_at: datetime
|
|
513
|
-
started_at_days_to_expiration: int
|
|
514
|
-
duration: int
|
|
515
|
-
total_opening_cost: Decimal
|
|
516
|
-
total_closing_cost: Decimal
|
|
517
|
-
total_cost: Decimal
|
|
518
|
-
gcd_open_quantity: Decimal
|
|
519
|
-
fees_missing: bool
|
|
520
|
-
open_entries: list[OrderChainEntry]
|
|
521
|
-
total_cost_per_unit: Decimal | None = None
|
|
522
|
-
|
|
523
|
-
@model_validator(mode="before")
|
|
524
|
-
@classmethod
|
|
525
|
-
def validate_price_effects(cls, data: Any) -> Any:
|
|
526
|
-
return set_sign_for(
|
|
527
|
-
data,
|
|
528
|
-
[
|
|
529
|
-
"total_fees",
|
|
530
|
-
"total_commissions",
|
|
531
|
-
"realized_gain",
|
|
532
|
-
"realized_gain_with_fees",
|
|
533
|
-
"total_opening_cost",
|
|
534
|
-
"total_closing_cost",
|
|
535
|
-
"total_cost",
|
|
536
|
-
"total_cost_per_unit",
|
|
537
|
-
],
|
|
538
|
-
)
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
class OrderChain(TastytradeData):
|
|
542
|
-
"""
|
|
543
|
-
Dataclass containing information about an order chain: a group of orders
|
|
544
|
-
for a specific underlying, such as total P/L, rolls, current P/L in a
|
|
545
|
-
symbol, etc.
|
|
546
|
-
"""
|
|
547
|
-
|
|
548
|
-
id: int
|
|
549
|
-
account_number: str
|
|
550
|
-
description: str
|
|
551
|
-
underlying_symbol: str
|
|
552
|
-
computed_data: ComputedData
|
|
553
|
-
lite_nodes: list[OrderChainNode]
|
|
554
|
-
lite_nodes_sizes: int | None = None
|
|
555
|
-
updated_at: datetime | None = None
|
|
556
|
-
created_at: datetime | None = None
|
|
@@ -233,7 +233,7 @@ class Customer(TastytradeData):
|
|
|
233
233
|
desk_customer_id: str | None = None
|
|
234
234
|
entity: CustomerEntity | None = None
|
|
235
235
|
family_member_names: str | None = None
|
|
236
|
-
has_institutional_assets: str | None = None
|
|
236
|
+
has_institutional_assets: str | bool | None = None
|
|
237
237
|
industry_affiliation_firm: str | None = None
|
|
238
238
|
is_investment_adviser: bool | None = None
|
|
239
239
|
listed_affiliation_symbol: str | None = None
|
|
@@ -289,9 +289,7 @@ class Session:
|
|
|
289
289
|
self.streamer_expiration = now_in_new_york()
|
|
290
290
|
self.refresh()
|
|
291
291
|
|
|
292
|
-
def _streamer_refresh(self) -> None:
|
|
293
|
-
# Pull streamer tokens and urls
|
|
294
|
-
data = self._get("/api-quote-tokens")
|
|
292
|
+
def _streamer_refresh(self, data: Any) -> None:
|
|
295
293
|
# Auth token for dxfeed websocket
|
|
296
294
|
self.streamer_token = data["token"]
|
|
297
295
|
# URL for dxfeed websocket
|
|
@@ -331,7 +329,8 @@ class Session:
|
|
|
331
329
|
self.async_client.headers.update(auth_headers)
|
|
332
330
|
# update the streamer token if necessary
|
|
333
331
|
if not self.is_test and self.streamer_expiration < self.session_expiration:
|
|
334
|
-
self.
|
|
332
|
+
data = self._get("/api-quote-tokens")
|
|
333
|
+
self._streamer_refresh(data)
|
|
335
334
|
|
|
336
335
|
async def a_refresh(self) -> None:
|
|
337
336
|
"""
|
|
@@ -365,13 +364,7 @@ class Session:
|
|
|
365
364
|
if not self.is_test and self.streamer_expiration < self.session_expiration:
|
|
366
365
|
# Pull streamer tokens and urls
|
|
367
366
|
data = await self._a_get("/api-quote-tokens")
|
|
368
|
-
|
|
369
|
-
self.streamer_token = data["token"]
|
|
370
|
-
# URL for dxfeed websocket
|
|
371
|
-
self.dxlink_url = data["dxlink-url"]
|
|
372
|
-
self.streamer_expiration = datetime.fromisoformat(
|
|
373
|
-
data["expires-at"].replace("Z", "+00:00")
|
|
374
|
-
)
|
|
367
|
+
self._streamer_refresh(data)
|
|
375
368
|
|
|
376
369
|
async def a_get_customer(self) -> Customer:
|
|
377
370
|
"""
|
|
@@ -29,12 +29,7 @@ from tastytrade.dxfeed import (
|
|
|
29
29
|
Trade,
|
|
30
30
|
Underlying,
|
|
31
31
|
)
|
|
32
|
-
from tastytrade.order import
|
|
33
|
-
InstrumentType,
|
|
34
|
-
OrderChain,
|
|
35
|
-
PlacedComplexOrder,
|
|
36
|
-
PlacedOrder,
|
|
37
|
-
)
|
|
32
|
+
from tastytrade.order import InstrumentType, PlacedComplexOrder, PlacedOrder
|
|
38
33
|
from tastytrade.session import Session
|
|
39
34
|
from tastytrade.utils import TastytradeData, TastytradeError, set_sign_for
|
|
40
35
|
from tastytrade.watchlists import Watchlist
|
|
@@ -133,7 +128,6 @@ AlertType: TypeAlias = (
|
|
|
133
128
|
| ExternalTransaction
|
|
134
129
|
| PlacedComplexOrder
|
|
135
130
|
| PlacedOrder
|
|
136
|
-
| OrderChain
|
|
137
131
|
| CurrentPosition
|
|
138
132
|
| QuoteAlert
|
|
139
133
|
| TradingStatus
|
|
@@ -146,7 +140,6 @@ MAP_ALERTS: dict[str, type[AlertType]] = {
|
|
|
146
140
|
"ComplexOrder": PlacedComplexOrder,
|
|
147
141
|
"ExternalTransaction": ExternalTransaction,
|
|
148
142
|
"Order": PlacedOrder,
|
|
149
|
-
"OrderChain": OrderChain,
|
|
150
143
|
"CurrentPosition": CurrentPosition,
|
|
151
144
|
"QuoteAlert": QuoteAlert,
|
|
152
145
|
"TradingStatus": TradingStatus,
|
|
@@ -761,7 +754,7 @@ class DXLinkStreamer:
|
|
|
761
754
|
}
|
|
762
755
|
|
|
763
756
|
def dict_from_schema(event_class: Any) -> dict[str, list[Any]]:
|
|
764
|
-
schema = event_class.
|
|
757
|
+
schema = event_class.model_json_schema()
|
|
765
758
|
return {schema["title"]: list(schema["properties"].keys())}
|
|
766
759
|
|
|
767
760
|
cls = MAP_EVENTS[event_type]
|
|
@@ -6,6 +6,7 @@ from typing import Any, Type, TypeVar, cast
|
|
|
6
6
|
from zoneinfo import ZoneInfo
|
|
7
7
|
|
|
8
8
|
from httpx import AsyncClient, Client, Response
|
|
9
|
+
from pandas import Timestamp
|
|
9
10
|
from pandas_market_calendars import get_calendar # type: ignore[import-untyped]
|
|
10
11
|
from pydantic import BaseModel, ConfigDict
|
|
11
12
|
|
|
@@ -40,6 +41,22 @@ def today_in_new_york() -> date:
|
|
|
40
41
|
return now_in_new_york().date()
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
def is_market_open_now() -> bool:
|
|
45
|
+
"""
|
|
46
|
+
Check if the market is currently open.
|
|
47
|
+
"""
|
|
48
|
+
today = today_in_new_york()
|
|
49
|
+
sched = NYSE.schedule(start_date=today, end_date=today)
|
|
50
|
+
if sched.empty:
|
|
51
|
+
# Closed full day (weekend or holiday)
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# Use iloc[0] since schedule has only one row for a single day
|
|
55
|
+
market_open: Timestamp = sched.iloc[0]["market_open"]
|
|
56
|
+
market_close: Timestamp = sched.iloc[0]["market_close"]
|
|
57
|
+
return market_open <= now_in_new_york() < market_close
|
|
58
|
+
|
|
59
|
+
|
|
43
60
|
def is_market_open_on(day: date | None = None) -> bool:
|
|
44
61
|
"""
|
|
45
62
|
Returns whether the market was/is/will be open at ANY point
|
|
@@ -3,7 +3,7 @@ from datetime import datetime
|
|
|
3
3
|
from decimal import Decimal
|
|
4
4
|
from time import sleep
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import pytest
|
|
7
7
|
|
|
8
8
|
from tastytrade import Account, Session
|
|
9
9
|
from tastytrade.instruments import Equity
|
|
@@ -15,17 +15,16 @@ from tastytrade.order import (
|
|
|
15
15
|
OrderType,
|
|
16
16
|
PlacedOrder,
|
|
17
17
|
)
|
|
18
|
-
from tastytrade.utils import TastytradeError
|
|
19
18
|
|
|
20
19
|
|
|
21
|
-
@fixture(scope="module")
|
|
20
|
+
@pytest.fixture(scope="module")
|
|
22
21
|
def account_number() -> str:
|
|
23
22
|
account_number = os.getenv("TT_ACCOUNT")
|
|
24
23
|
assert account_number is not None
|
|
25
24
|
return account_number
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
@fixture(scope="module")
|
|
27
|
+
@pytest.fixture(scope="module")
|
|
29
28
|
async def account(session: Session, account_number: str, aiolib: str) -> Account:
|
|
30
29
|
return Account.get(session, account_number)
|
|
31
30
|
|
|
@@ -55,27 +54,37 @@ def test_get_positions(session: Session, account: Account):
|
|
|
55
54
|
|
|
56
55
|
|
|
57
56
|
def test_get_history(session: Session, account: Account):
|
|
58
|
-
account.get_history(session, page_offset=0)
|
|
57
|
+
hist = account.get_history(session, page_offset=0)
|
|
58
|
+
tid = hist[0].id
|
|
59
|
+
account.get_transaction(session, tid)
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
def test_get_total_fees(session: Session, account: Account):
|
|
62
63
|
account.get_total_fees(session)
|
|
63
64
|
|
|
64
65
|
|
|
65
|
-
def test_get_position_limit(session: Session, account: Account):
|
|
66
|
-
account.get_position_limit(session)
|
|
67
|
-
|
|
68
|
-
|
|
69
66
|
def test_get_margin_requirements(session: Session, account: Account):
|
|
70
67
|
account.get_margin_requirements(session)
|
|
71
68
|
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
@pytest.mark.parametrize(
|
|
71
|
+
"time_back, start_time",
|
|
72
|
+
[
|
|
73
|
+
("1y", None),
|
|
74
|
+
(None, datetime(2024, 1, 1)),
|
|
75
|
+
pytest.param(None, None, marks=pytest.mark.xfail, id="intentional_fail"),
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
def test_get_net_liquidating_value_history(
|
|
79
|
+
session: Session,
|
|
80
|
+
account: Account,
|
|
81
|
+
time_back: str | None,
|
|
82
|
+
start_time: datetime | None,
|
|
83
|
+
):
|
|
84
|
+
sleep(1)
|
|
85
|
+
account.get_net_liquidating_value_history(
|
|
86
|
+
session, time_back=time_back, start_time=start_time
|
|
87
|
+
)
|
|
79
88
|
|
|
80
89
|
|
|
81
90
|
def test_get_order_history(session: Session, account: Account):
|
|
@@ -116,31 +125,37 @@ async def test_get_positions_async(session: Session, account: Account):
|
|
|
116
125
|
|
|
117
126
|
|
|
118
127
|
async def test_get_history_async(session: Session, account: Account):
|
|
119
|
-
await account.a_get_history(session, page_offset=0)
|
|
128
|
+
hist = await account.a_get_history(session, page_offset=0)
|
|
129
|
+
tid = hist[0].id
|
|
130
|
+
await account.a_get_transaction(session, tid)
|
|
120
131
|
|
|
121
132
|
|
|
122
133
|
async def test_get_total_fees_async(session: Session, account: Account):
|
|
123
134
|
await account.a_get_total_fees(session)
|
|
124
135
|
|
|
125
136
|
|
|
126
|
-
async def test_get_position_limit_async(session: Session, account: Account):
|
|
127
|
-
await account.a_get_position_limit(session)
|
|
128
|
-
|
|
129
|
-
|
|
130
137
|
async def test_get_margin_requirements_async(session: Session, account: Account):
|
|
131
138
|
await account.a_get_margin_requirements(session)
|
|
132
139
|
|
|
133
140
|
|
|
141
|
+
@pytest.mark.parametrize(
|
|
142
|
+
"time_back, start_time",
|
|
143
|
+
[
|
|
144
|
+
("1y", None),
|
|
145
|
+
(None, datetime(2024, 1, 1)),
|
|
146
|
+
pytest.param(None, None, marks=pytest.mark.xfail, id="intentional_fail"),
|
|
147
|
+
],
|
|
148
|
+
)
|
|
134
149
|
async def test_get_net_liquidating_value_history_async(
|
|
135
|
-
session: Session,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
async def test_get_effective_margin_requirements_async(
|
|
141
|
-
session: Session, account: Account
|
|
150
|
+
session: Session,
|
|
151
|
+
account: Account,
|
|
152
|
+
time_back: str | None,
|
|
153
|
+
start_time: datetime | None,
|
|
142
154
|
):
|
|
143
|
-
|
|
155
|
+
sleep(1)
|
|
156
|
+
await account.a_get_net_liquidating_value_history(
|
|
157
|
+
session, time_back=time_back, start_time=start_time
|
|
158
|
+
)
|
|
144
159
|
|
|
145
160
|
|
|
146
161
|
async def test_get_order_history_async(session: Session, account: Account):
|
|
@@ -155,26 +170,10 @@ async def test_get_live_orders_async(session: Session, account: Account):
|
|
|
155
170
|
await account.a_get_live_orders(session)
|
|
156
171
|
|
|
157
172
|
|
|
158
|
-
|
|
159
|
-
start_time = datetime(2024, 1, 1, 0, 0, 0)
|
|
160
|
-
end_time = datetime.now()
|
|
161
|
-
with raises(TastytradeError):
|
|
162
|
-
account.get_order_chains(session, "F", start_time=start_time, end_time=end_time)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
async def test_get_order_chains_async(session: Session, account: Account):
|
|
166
|
-
start_time = datetime(2024, 1, 1, 0, 0, 0)
|
|
167
|
-
end_time = datetime.now()
|
|
168
|
-
with raises(TastytradeError):
|
|
169
|
-
await account.a_get_order_chains(
|
|
170
|
-
session, "F", start_time=start_time, end_time=end_time
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
@fixture(scope="module")
|
|
173
|
+
@pytest.fixture(scope="module")
|
|
175
174
|
def new_order(session: Session) -> NewOrder:
|
|
176
175
|
symbol = Equity.get(session, "F")
|
|
177
|
-
leg = symbol.build_leg(
|
|
176
|
+
leg = symbol.build_leg(1, OrderAction.BUY_TO_OPEN)
|
|
178
177
|
return NewOrder(
|
|
179
178
|
time_in_force=OrderTimeInForce.DAY,
|
|
180
179
|
order_type=OrderType.LIMIT,
|
|
@@ -183,7 +182,7 @@ def new_order(session: Session) -> NewOrder:
|
|
|
183
182
|
)
|
|
184
183
|
|
|
185
184
|
|
|
186
|
-
@fixture(scope="module")
|
|
185
|
+
@pytest.fixture(scope="module")
|
|
187
186
|
def notional_order(session: Session) -> NewOrder:
|
|
188
187
|
symbol = Equity.get(session, "AAPL")
|
|
189
188
|
leg = symbol.build_leg(None, OrderAction.BUY_TO_OPEN)
|
|
@@ -195,7 +194,7 @@ def notional_order(session: Session) -> NewOrder:
|
|
|
195
194
|
)
|
|
196
195
|
|
|
197
196
|
|
|
198
|
-
@fixture(scope="module")
|
|
197
|
+
@pytest.fixture(scope="module")
|
|
199
198
|
def placed_order(
|
|
200
199
|
session: Session, account: Account, new_order: NewOrder
|
|
201
200
|
) -> PlacedOrder:
|
|
@@ -230,7 +229,7 @@ def test_replace_and_delete_order(
|
|
|
230
229
|
def test_place_oco_order(session: Session, account: Account):
|
|
231
230
|
# account must have a share of F for this to work
|
|
232
231
|
symbol = Equity.get(session, "F")
|
|
233
|
-
closing = symbol.build_leg(
|
|
232
|
+
closing = symbol.build_leg(1, OrderAction.SELL_TO_CLOSE)
|
|
234
233
|
oco = NewComplexOrder(
|
|
235
234
|
orders=[
|
|
236
235
|
NewOrder(
|
|
@@ -256,8 +255,8 @@ def test_place_oco_order(session: Session, account: Account):
|
|
|
256
255
|
|
|
257
256
|
def test_place_otoco_order(session: Session, account: Account):
|
|
258
257
|
symbol = Equity.get(session, "AAPL")
|
|
259
|
-
opening = symbol.build_leg(
|
|
260
|
-
closing = symbol.build_leg(
|
|
258
|
+
opening = symbol.build_leg(1, OrderAction.BUY_TO_OPEN)
|
|
259
|
+
closing = symbol.build_leg(1, OrderAction.SELL_TO_CLOSE)
|
|
261
260
|
otoco = NewComplexOrder(
|
|
262
261
|
trigger_order=NewOrder(
|
|
263
262
|
time_in_force=OrderTimeInForce.DAY,
|
|
@@ -290,7 +289,7 @@ def test_get_live_complex_orders(session: Session, account: Account):
|
|
|
290
289
|
assert orders != []
|
|
291
290
|
|
|
292
291
|
|
|
293
|
-
@fixture(scope="module")
|
|
292
|
+
@pytest.fixture(scope="module")
|
|
294
293
|
async def placed_order_async(
|
|
295
294
|
session: Session, account: Account, new_order: NewOrder
|
|
296
295
|
) -> PlacedOrder:
|
|
@@ -328,8 +327,8 @@ async def test_replace_and_delete_order_async(
|
|
|
328
327
|
async def test_place_complex_order_async(session: Session, account: Account):
|
|
329
328
|
sleep(3)
|
|
330
329
|
symbol = Equity.get(session, "AAPL")
|
|
331
|
-
opening = symbol.build_leg(
|
|
332
|
-
closing = symbol.build_leg(
|
|
330
|
+
opening = symbol.build_leg(1, OrderAction.BUY_TO_OPEN)
|
|
331
|
+
closing = symbol.build_leg(1, OrderAction.SELL_TO_CLOSE)
|
|
333
332
|
otoco = NewComplexOrder(
|
|
334
333
|
trigger_order=NewOrder(
|
|
335
334
|
time_in_force=OrderTimeInForce.DAY,
|
|
@@ -360,3 +359,61 @@ async def test_place_complex_order_async(session: Session, account: Account):
|
|
|
360
359
|
async def test_get_live_complex_orders_async(session: Session, account: Account):
|
|
361
360
|
orders = await account.a_get_live_complex_orders(session)
|
|
362
361
|
assert orders != []
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
async def test_place_oco_order_async(session: Session, account: Account):
|
|
365
|
+
# account must have a share of F for this to work
|
|
366
|
+
symbol = await Equity.a_get(session, "F")
|
|
367
|
+
closing = symbol.build_leg(1, OrderAction.SELL_TO_CLOSE)
|
|
368
|
+
oco = NewComplexOrder(
|
|
369
|
+
orders=[
|
|
370
|
+
NewOrder(
|
|
371
|
+
time_in_force=OrderTimeInForce.GTC,
|
|
372
|
+
order_type=OrderType.LIMIT,
|
|
373
|
+
legs=[closing],
|
|
374
|
+
price=Decimal(1000), # will never fill
|
|
375
|
+
),
|
|
376
|
+
NewOrder(
|
|
377
|
+
time_in_force=OrderTimeInForce.GTC,
|
|
378
|
+
order_type=OrderType.STOP,
|
|
379
|
+
legs=[closing],
|
|
380
|
+
stop_trigger=Decimal(1), # will never fill
|
|
381
|
+
),
|
|
382
|
+
]
|
|
383
|
+
)
|
|
384
|
+
resp2 = await account.a_place_complex_order(session, oco, dry_run=False)
|
|
385
|
+
sleep(3)
|
|
386
|
+
# test get complex order
|
|
387
|
+
_ = await account.a_get_complex_order(session, resp2.complex_order.id)
|
|
388
|
+
await account.a_delete_complex_order(session, resp2.complex_order.id)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
async def test_place_otoco_order_async(session: Session, account: Account):
|
|
392
|
+
symbol = await Equity.a_get(session, "AAPL")
|
|
393
|
+
opening = symbol.build_leg(1, OrderAction.BUY_TO_OPEN)
|
|
394
|
+
closing = symbol.build_leg(1, OrderAction.SELL_TO_CLOSE)
|
|
395
|
+
otoco = NewComplexOrder(
|
|
396
|
+
trigger_order=NewOrder(
|
|
397
|
+
time_in_force=OrderTimeInForce.DAY,
|
|
398
|
+
order_type=OrderType.LIMIT,
|
|
399
|
+
legs=[opening],
|
|
400
|
+
price=Decimal("-2"), # won't fill
|
|
401
|
+
),
|
|
402
|
+
orders=[
|
|
403
|
+
NewOrder(
|
|
404
|
+
time_in_force=OrderTimeInForce.GTC,
|
|
405
|
+
order_type=OrderType.LIMIT,
|
|
406
|
+
legs=[closing],
|
|
407
|
+
price=Decimal(400), # won't fill
|
|
408
|
+
),
|
|
409
|
+
NewOrder(
|
|
410
|
+
time_in_force=OrderTimeInForce.GTC,
|
|
411
|
+
order_type=OrderType.STOP,
|
|
412
|
+
legs=[closing],
|
|
413
|
+
stop_trigger=Decimal("1.5"), # won't fill
|
|
414
|
+
),
|
|
415
|
+
],
|
|
416
|
+
)
|
|
417
|
+
resp = await account.a_place_complex_order(session, otoco, dry_run=False)
|
|
418
|
+
sleep(3)
|
|
419
|
+
await account.a_delete_complex_order(session, resp.complex_order.id)
|
|
@@ -29,21 +29,14 @@ def test_parse_infinities_and_nan():
|
|
|
29
29
|
assert summary.day_high_price is None
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
quote_data = ["SPY", 0, 0, 0, 0, "Q", 0, "Q", 576.88, 576.9, 230.0, 300.0]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_wrong_number_data_fields():
|
|
33
36
|
with pytest.raises(TastytradeError):
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"Q",
|
|
41
|
-
0,
|
|
42
|
-
"Q",
|
|
43
|
-
576.88,
|
|
44
|
-
576.9,
|
|
45
|
-
230.0,
|
|
46
|
-
300.0,
|
|
47
|
-
"extra",
|
|
48
|
-
]
|
|
49
|
-
_ = Quote.from_stream(quote_data)
|
|
37
|
+
_ = Quote.from_stream(quote_data + ["extra"])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_bad_extra_data():
|
|
41
|
+
extra_data = ["SPY", 0, "bad", 0, 0, "Q", 0, "Q", 576.88, 576.9, 230.0, 300.0]
|
|
42
|
+
_ = Quote.from_stream(quote_data + extra_data)
|
|
@@ -3,8 +3,10 @@ from pytest import fixture
|
|
|
3
3
|
from tastytrade import Session
|
|
4
4
|
from tastytrade.market_sessions import (
|
|
5
5
|
ExchangeType,
|
|
6
|
+
a_get_futures_holidays,
|
|
6
7
|
a_get_market_holidays,
|
|
7
8
|
a_get_market_sessions,
|
|
9
|
+
get_futures_holidays,
|
|
8
10
|
get_market_holidays,
|
|
9
11
|
get_market_sessions,
|
|
10
12
|
)
|
|
@@ -25,9 +27,17 @@ async def test_get_market_holidays_async(session: Session):
|
|
|
25
27
|
await a_get_market_holidays(session)
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
async def test_get_future_holidays_async(session: Session):
|
|
31
|
+
await a_get_futures_holidays(session, ExchangeType.CME)
|
|
32
|
+
|
|
33
|
+
|
|
28
34
|
def test_get_market_sessions(session: Session, exchanges: list[ExchangeType]):
|
|
29
35
|
get_market_sessions(session, exchanges=exchanges)
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
def test_get_market_holidays(session: Session):
|
|
33
39
|
get_market_holidays(session)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_get_future_holidays(session: Session):
|
|
43
|
+
get_futures_holidays(session, ExchangeType.CME)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
from datetime import date
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
from unittest.mock import patch
|
|
2
3
|
|
|
3
4
|
from tastytrade.utils import (
|
|
5
|
+
TZ,
|
|
4
6
|
get_future_fx_monthly,
|
|
5
7
|
get_future_grain_monthly,
|
|
6
8
|
get_future_index_monthly,
|
|
@@ -9,6 +11,7 @@ from tastytrade.utils import (
|
|
|
9
11
|
get_future_treasury_monthly,
|
|
10
12
|
get_tasty_monthly,
|
|
11
13
|
get_third_friday,
|
|
14
|
+
is_market_open_now,
|
|
12
15
|
today_in_new_york,
|
|
13
16
|
)
|
|
14
17
|
|
|
@@ -158,3 +161,68 @@ def test_get_future_index_monthly():
|
|
|
158
161
|
]
|
|
159
162
|
for exp in exps:
|
|
160
163
|
assert get_future_index_monthly(exp) == exp
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_is_market_open_now():
|
|
167
|
+
# Use a known Tuesday (March 12, 2024) for weekday tests
|
|
168
|
+
tuesday = datetime(2024, 3, 12, 10, 0, 0, tzinfo=TZ)
|
|
169
|
+
|
|
170
|
+
# Test market open during trading hours (10:00 AM on a weekday)
|
|
171
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
172
|
+
mock_datetime.now.return_value = tuesday
|
|
173
|
+
assert is_market_open_now() is True
|
|
174
|
+
|
|
175
|
+
# Test market closed before opening (8:00 AM on a weekday)
|
|
176
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
177
|
+
mock_datetime.now.return_value = datetime(2024, 3, 12, 8, 0, 0, tzinfo=TZ)
|
|
178
|
+
assert is_market_open_now() is False
|
|
179
|
+
|
|
180
|
+
# Test market closed after closing (5:00 PM on a weekday)
|
|
181
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
182
|
+
mock_datetime.now.return_value = datetime(2024, 3, 12, 17, 0, 0, tzinfo=TZ)
|
|
183
|
+
assert is_market_open_now() is False
|
|
184
|
+
|
|
185
|
+
# Test market closed on weekend (Saturday, March 16, 2024)
|
|
186
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
187
|
+
mock_datetime.now.return_value = datetime(2024, 3, 16, 12, 0, 0, tzinfo=TZ)
|
|
188
|
+
assert is_market_open_now() is False
|
|
189
|
+
|
|
190
|
+
# Test edge case: exactly 9:30 AM (market opens)
|
|
191
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
192
|
+
mock_datetime.now.return_value = datetime(2024, 3, 12, 9, 30, 0, tzinfo=TZ)
|
|
193
|
+
assert is_market_open_now() is True
|
|
194
|
+
|
|
195
|
+
# Test edge case: just before 9:30 AM (9:29 AM)
|
|
196
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
197
|
+
mock_datetime.now.return_value = datetime(2024, 3, 12, 9, 29, 0, tzinfo=TZ)
|
|
198
|
+
assert is_market_open_now() is False
|
|
199
|
+
|
|
200
|
+
# Test edge case: exactly 4:00 PM (market closes, should be False)
|
|
201
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
202
|
+
mock_datetime.now.return_value = datetime(2024, 3, 12, 16, 0, 0, tzinfo=TZ)
|
|
203
|
+
assert is_market_open_now() is False
|
|
204
|
+
|
|
205
|
+
# Test edge case: just before 4:00 PM (3:59 PM)
|
|
206
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
207
|
+
mock_datetime.now.return_value = datetime(2024, 3, 12, 15, 59, 0, tzinfo=TZ)
|
|
208
|
+
assert is_market_open_now() is True
|
|
209
|
+
|
|
210
|
+
# Test edge case: Black Friday just before market open
|
|
211
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
212
|
+
mock_datetime.now.return_value = datetime(2024, 11, 29, 9, 29, 0, tzinfo=TZ)
|
|
213
|
+
assert is_market_open_now() is False
|
|
214
|
+
|
|
215
|
+
# Test edge case: Black Friday just after market open
|
|
216
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
217
|
+
mock_datetime.now.return_value = datetime(2024, 11, 29, 9, 30, 0, tzinfo=TZ)
|
|
218
|
+
assert is_market_open_now() is True
|
|
219
|
+
|
|
220
|
+
# Test edge case: Black Friday just before market close half day
|
|
221
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
222
|
+
mock_datetime.now.return_value = datetime(2024, 11, 29, 12, 59, 0, tzinfo=TZ)
|
|
223
|
+
assert is_market_open_now() is True
|
|
224
|
+
|
|
225
|
+
# Test edge case: Black Friday just after market close half day
|
|
226
|
+
with patch("tastytrade.utils.datetime") as mock_datetime:
|
|
227
|
+
mock_datetime.now.return_value = datetime(2024, 11, 29, 13, 0, 0, tzinfo=TZ)
|
|
228
|
+
assert is_market_open_now() is False
|
|
@@ -1161,6 +1161,42 @@ wheels = [
|
|
|
1161
1161
|
{ url = "https://files.pythonhosted.org/packages/09/c3/4410606429c38a5c62597b903c65706de4b163a3c40a6a0308636a43cc30/pandas_market_calendars-5.1.1-py3-none-any.whl", hash = "sha256:efe85e335c927824e2714a70f8ba769ef4bbebab924bd13e7f80180262d5f7ee", size = 127359, upload-time = "2025-06-22T17:38:16.657Z" },
|
|
1162
1162
|
]
|
|
1163
1163
|
|
|
1164
|
+
[[package]]
|
|
1165
|
+
name = "pandas-stubs"
|
|
1166
|
+
version = "2.2.2.240807"
|
|
1167
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1168
|
+
resolution-markers = [
|
|
1169
|
+
"python_full_version < '3.10'",
|
|
1170
|
+
]
|
|
1171
|
+
dependencies = [
|
|
1172
|
+
{ name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
|
1173
|
+
{ name = "types-pytz", marker = "python_full_version < '3.10'" },
|
|
1174
|
+
]
|
|
1175
|
+
sdist = { url = "https://files.pythonhosted.org/packages/1f/df/0da95bc75c76f1e012e0bc0b76da31faaf4254e94b9870f25e6311145e98/pandas_stubs-2.2.2.240807.tar.gz", hash = "sha256:64a559725a57a449f46225fbafc422520b7410bff9252b661a225b5559192a93", size = 103095, upload-time = "2024-08-07T12:30:54.538Z" }
|
|
1176
|
+
wheels = [
|
|
1177
|
+
{ url = "https://files.pythonhosted.org/packages/0a/f9/22c91632ea1b4c6165952f677bf9ad95f9ac36ffd7ef3e6450144e6d8b1a/pandas_stubs-2.2.2.240807-py3-none-any.whl", hash = "sha256:893919ad82be4275f0d07bb47a95d08bae580d3fdea308a7acfcb3f02e76186e", size = 157069, upload-time = "2024-08-07T12:30:51.868Z" },
|
|
1178
|
+
]
|
|
1179
|
+
|
|
1180
|
+
[[package]]
|
|
1181
|
+
name = "pandas-stubs"
|
|
1182
|
+
version = "2.3.3.251201"
|
|
1183
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1184
|
+
resolution-markers = [
|
|
1185
|
+
"python_full_version >= '3.13'",
|
|
1186
|
+
"python_full_version == '3.12.*'",
|
|
1187
|
+
"python_full_version == '3.11.*'",
|
|
1188
|
+
"python_full_version == '3.10.*'",
|
|
1189
|
+
]
|
|
1190
|
+
dependencies = [
|
|
1191
|
+
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
|
|
1192
|
+
{ name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
1193
|
+
{ name = "types-pytz", marker = "python_full_version >= '3.10'" },
|
|
1194
|
+
]
|
|
1195
|
+
sdist = { url = "https://files.pythonhosted.org/packages/ee/a6/491b2af2cb3ee232765a73fb273a44cc1ac33b154f7745b2df2ee1dc4d01/pandas_stubs-2.3.3.251201.tar.gz", hash = "sha256:7a980f4f08cff2a6d7e4c6d6d26f4c5fcdb82a6f6531489b2f75c81567fe4536", size = 107787, upload-time = "2025-12-01T18:29:22.403Z" }
|
|
1196
|
+
wheels = [
|
|
1197
|
+
{ url = "https://files.pythonhosted.org/packages/e2/68/78a3c253f146254b8e2c19f4a4768f272e12ef11001d9b45ec7b165db054/pandas_stubs-2.3.3.251201-py3-none-any.whl", hash = "sha256:eb5c9b6138bd8492fd74a47b09c9497341a278fcfbc8633ea4b35b230ebf4be5", size = 164638, upload-time = "2025-12-01T18:29:21.006Z" },
|
|
1198
|
+
]
|
|
1199
|
+
|
|
1164
1200
|
[[package]]
|
|
1165
1201
|
name = "pathspec"
|
|
1166
1202
|
version = "0.12.1"
|
|
@@ -1994,6 +2030,8 @@ dev = [
|
|
|
1994
2030
|
{ name = "autodoc-pydantic" },
|
|
1995
2031
|
{ name = "enum-tools", extra = ["sphinx"] },
|
|
1996
2032
|
{ name = "mypy" },
|
|
2033
|
+
{ name = "pandas-stubs", version = "2.2.2.240807", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
|
2034
|
+
{ name = "pandas-stubs", version = "2.3.3.251201", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
|
1997
2035
|
{ name = "proxy-py" },
|
|
1998
2036
|
{ name = "pyright" },
|
|
1999
2037
|
{ name = "pytest" },
|
|
@@ -2020,6 +2058,7 @@ dev = [
|
|
|
2020
2058
|
{ name = "autodoc-pydantic", specifier = ">=2.2.0" },
|
|
2021
2059
|
{ name = "enum-tools", extras = ["sphinx"], specifier = ">=0.12.0" },
|
|
2022
2060
|
{ name = "mypy", specifier = ">=1.18.2" },
|
|
2061
|
+
{ name = "pandas-stubs", specifier = ">=2.2.2.240807" },
|
|
2023
2062
|
{ name = "proxy-py", specifier = ">=2.4.9" },
|
|
2024
2063
|
{ name = "pyright", specifier = ">=1.1.401" },
|
|
2025
2064
|
{ name = "pytest", specifier = ">=8.3.3" },
|
|
@@ -2078,6 +2117,15 @@ wheels = [
|
|
|
2078
2117
|
{ url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383, upload-time = "2024-10-04T16:17:01.533Z" },
|
|
2079
2118
|
]
|
|
2080
2119
|
|
|
2120
|
+
[[package]]
|
|
2121
|
+
name = "types-pytz"
|
|
2122
|
+
version = "2025.2.0.20251108"
|
|
2123
|
+
source = { registry = "https://pypi.org/simple" }
|
|
2124
|
+
sdist = { url = "https://files.pythonhosted.org/packages/40/ff/c047ddc68c803b46470a357454ef76f4acd8c1088f5cc4891cdd909bfcf6/types_pytz-2025.2.0.20251108.tar.gz", hash = "sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb", size = 10961, upload-time = "2025-11-08T02:55:57.001Z" }
|
|
2125
|
+
wheels = [
|
|
2126
|
+
{ url = "https://files.pythonhosted.org/packages/e7/c1/56ef16bf5dcd255155cc736d276efa6ae0a5c26fd685e28f0412a4013c01/types_pytz-2025.2.0.20251108-py3-none-any.whl", hash = "sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c", size = 10116, upload-time = "2025-11-08T02:55:56.194Z" },
|
|
2127
|
+
]
|
|
2128
|
+
|
|
2081
2129
|
[[package]]
|
|
2082
2130
|
name = "typing-extensions"
|
|
2083
2131
|
version = "4.15.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|