tastytrade 11.0.3__tar.gz → 11.0.5__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.3 → tastytrade-11.0.5}/PKG-INFO +1 -1
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/sessions.rst +0 -9
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/__init__.py +1 -1
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/account.py +0 -2
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/event.py +0 -2
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/instruments.py +1 -1
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/order.py +6 -4
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/session.py +12 -27
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/utils.py +0 -24
- {tastytrade-11.0.3 → tastytrade-11.0.5}/uv.lock +5 -3
- tastytrade-11.0.3/tastytrade/oauth.py +0 -129
- tastytrade-11.0.3/tmp.py +0 -32
- {tastytrade-11.0.3 → tastytrade-11.0.5}/.github/CONTRIBUTING.md +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/.github/FUNDING.yml +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/.github/pull_request_template.md +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/.github/workflows/python-app.yml +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/.github/workflows/python-publish-test.yml +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/.github/workflows/python-publish.yml +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/.gitignore +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/.python-version +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/.readthedocs.yaml +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/LICENSE +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/Makefile +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/README.md +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/Makefile +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/account-streamer.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/accounts.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/account.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/dxfeed.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/instruments.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/market-data.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/market-sessions.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/metrics.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/order.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/search.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/session.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/streamer.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/utils.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/api/watchlists.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/conf.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/data-streamer.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/img/netliq.png +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/index.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/installation.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/instruments.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/make.bat +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/market-data.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/market-sessions.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/orders.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/sync-async.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/docs/watchlists.rst +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/pyproject.toml +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/__init__.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/candle.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/greeks.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/profile.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/quote.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/summary.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/theoprice.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/timeandsale.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/trade.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/dxfeed/underlying.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/market_data.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/market_sessions.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/metrics.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/py.typed +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/search.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/streamer.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tastytrade/watchlists.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/__init__.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/conftest.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_account.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_dxfeed.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_instruments.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_market_data.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_market_sessions.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_metrics.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_search.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_session.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_streamer.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_utils.py +0 -0
- {tastytrade-11.0.3 → tastytrade-11.0.5}/tests/test_watchlists.py +0 -0
|
@@ -13,15 +13,6 @@ Generating an initial refresh token
|
|
|
13
13
|
|
|
14
14
|
In order to generate an initial refresh token, you have two options. The easiest way is to simply generate one from Tastytrade's website: go to OAuth Applications > Manage > Create Grant to get a new refresh token, **which you should also save**.
|
|
15
15
|
|
|
16
|
-
The other option (which is actually the only option for sandbox accounts) is to run this helper code:
|
|
17
|
-
|
|
18
|
-
.. code-block:: python
|
|
19
|
-
|
|
20
|
-
from tastytrade.oauth import login
|
|
21
|
-
login(is_test=True)
|
|
22
|
-
|
|
23
|
-
This will open up a web interface in your browser where you'll be prompted to paste your client ID and client secret. These credentials will then be used to connect your application to Tastytrade. After following the steps in your browser, you should see your new refresh token in the browser and in the console.
|
|
24
|
-
|
|
25
16
|
At this point, OAuth is now setup correctly! Doing these steps once is sufficient for **indefinite usage** of ``Session`` for authentication to the API, since refresh tokens never expire. From now on you can simply authenticate with your client secret and refresh token.
|
|
26
17
|
|
|
27
18
|
Creating a session
|
|
@@ -4,7 +4,7 @@ API_URL = "https://api.tastyworks.com"
|
|
|
4
4
|
API_VERSION = "20251101"
|
|
5
5
|
CERT_URL = "https://api.cert.tastyworks.com"
|
|
6
6
|
VAST_URL = "https://vast.tastyworks.com"
|
|
7
|
-
VERSION = "11.0.
|
|
7
|
+
VERSION = "11.0.5"
|
|
8
8
|
|
|
9
9
|
__version__ = VERSION
|
|
10
10
|
version_str: str = f"tastyware/tastytrade:v{VERSION}"
|
|
@@ -546,8 +546,6 @@ class Account(TastytradeData):
|
|
|
546
546
|
:param session: the session to use for the request.
|
|
547
547
|
:param account_number: the account ID to get.
|
|
548
548
|
:param include_closed: whether to include closed accounts in the results
|
|
549
|
-
|
|
550
|
-
:return: an account if an ID was provided; otherwise, a single account.
|
|
551
549
|
"""
|
|
552
550
|
if account_number:
|
|
553
551
|
data = session._get(f"/customers/me/accounts/{account_number}")
|
|
@@ -490,8 +490,8 @@ class Option(TradeableTastytradeData):
|
|
|
490
490
|
stops_trading_at: datetime
|
|
491
491
|
market_time_instrument_collection: str
|
|
492
492
|
days_to_expiration: int
|
|
493
|
-
expires_at: datetime
|
|
494
493
|
is_closing_only: bool
|
|
494
|
+
expires_at: datetime | None = None
|
|
495
495
|
streamer_symbol: str = ""
|
|
496
496
|
listed_market: str | None = None
|
|
497
497
|
halted_at: datetime | None = None
|
|
@@ -133,7 +133,7 @@ class Leg(TastytradeData):
|
|
|
133
133
|
instrument_type: InstrumentType
|
|
134
134
|
symbol: str
|
|
135
135
|
action: OrderAction
|
|
136
|
-
quantity: Decimal | None = None
|
|
136
|
+
quantity: Decimal | int | None = None
|
|
137
137
|
remaining_quantity: Decimal | None = None
|
|
138
138
|
fills: list[FillInfo] | None = None
|
|
139
139
|
|
|
@@ -149,15 +149,13 @@ class TradeableTastytradeData(TastytradeData):
|
|
|
149
149
|
instrument_type: InstrumentType
|
|
150
150
|
symbol: str
|
|
151
151
|
|
|
152
|
-
def build_leg(self, quantity: Decimal | None, action: OrderAction) -> Leg:
|
|
152
|
+
def build_leg(self, quantity: Decimal | int | None, action: OrderAction) -> Leg:
|
|
153
153
|
"""
|
|
154
154
|
Builds an order :class:`Leg` from the dataclass.
|
|
155
155
|
|
|
156
156
|
:param quantity:
|
|
157
157
|
the quantity of the symbol to trade, set this as `None` for notional orders
|
|
158
158
|
:param action: :class:`OrderAction` to perform, e.g. BUY_TO_OPEN
|
|
159
|
-
|
|
160
|
-
:return: a :class:`Leg` object
|
|
161
159
|
"""
|
|
162
160
|
return Leg(
|
|
163
161
|
instrument_type=self.instrument_type,
|
|
@@ -254,6 +252,8 @@ class NewOrder(TastytradeData):
|
|
|
254
252
|
preflight_id: str | None = None
|
|
255
253
|
rules: OrderRule | None = None
|
|
256
254
|
advanced_instructions: AdvancedInstructions | None = None
|
|
255
|
+
#: External identifier for the order, used to track orders across systems
|
|
256
|
+
external_identifier: str | None = None
|
|
257
257
|
|
|
258
258
|
@computed_field # type: ignore[misc]
|
|
259
259
|
@property
|
|
@@ -330,6 +330,8 @@ class PlacedOrder(TastytradeData):
|
|
|
330
330
|
preflight_id: str | int | None = None
|
|
331
331
|
order_rule: OrderRule | None = None
|
|
332
332
|
source: str | None = None
|
|
333
|
+
#: External identifier for the order, used to track orders across systems
|
|
334
|
+
external_identifier: str | None = None
|
|
333
335
|
|
|
334
336
|
@model_validator(mode="before")
|
|
335
337
|
@classmethod
|
|
@@ -9,8 +9,8 @@ from typing_extensions import Self
|
|
|
9
9
|
|
|
10
10
|
from tastytrade import API_URL, API_VERSION, CERT_URL, logger
|
|
11
11
|
from tastytrade.utils import (
|
|
12
|
-
TZ,
|
|
13
12
|
TastytradeData,
|
|
13
|
+
now_in_new_york,
|
|
14
14
|
validate_and_parse,
|
|
15
15
|
validate_response,
|
|
16
16
|
)
|
|
@@ -274,11 +274,9 @@ class Session:
|
|
|
274
274
|
#: Refresh token for the user
|
|
275
275
|
self.refresh_token = refresh_token
|
|
276
276
|
# The headers to use for API requests
|
|
277
|
-
headers = {
|
|
278
|
-
|
|
279
|
-
"Accept-Version"
|
|
280
|
-
"Content-Type": "application/json",
|
|
281
|
-
}
|
|
277
|
+
headers = {"Accept": "application/json", "Content-Type": "application/json"}
|
|
278
|
+
if not is_test: # not accepted in sandbox
|
|
279
|
+
headers["Accept-Version"] = API_VERSION
|
|
282
280
|
#: httpx client for sync requests
|
|
283
281
|
self.sync_client = Client(
|
|
284
282
|
base_url=(CERT_URL if is_test else API_URL), headers=headers, proxy=proxy
|
|
@@ -288,7 +286,7 @@ class Session:
|
|
|
288
286
|
base_url=self.sync_client.base_url, headers=headers, proxy=proxy
|
|
289
287
|
)
|
|
290
288
|
#: expiration for streamer token
|
|
291
|
-
self.streamer_expiration =
|
|
289
|
+
self.streamer_expiration = now_in_new_york()
|
|
292
290
|
self.refresh()
|
|
293
291
|
|
|
294
292
|
def _streamer_refresh(self) -> None:
|
|
@@ -325,14 +323,14 @@ class Session:
|
|
|
325
323
|
self.session_token = data["access_token"]
|
|
326
324
|
token_lifetime: int = data.get("expires_in", 900)
|
|
327
325
|
#: expiration for session token
|
|
328
|
-
self.session_expiration =
|
|
326
|
+
self.session_expiration = now_in_new_york() + timedelta(seconds=token_lifetime)
|
|
329
327
|
logger.debug(f"Refreshed token, expires in {token_lifetime}ms")
|
|
330
328
|
auth_headers = {"Authorization": f"Bearer {self.session_token}"}
|
|
331
329
|
# update the httpx clients with the new token
|
|
332
330
|
self.sync_client.headers.update(auth_headers)
|
|
333
331
|
self.async_client.headers.update(auth_headers)
|
|
334
332
|
# update the streamer token if necessary
|
|
335
|
-
if self.streamer_expiration < self.session_expiration:
|
|
333
|
+
if not self.is_test and self.streamer_expiration < self.session_expiration:
|
|
336
334
|
self._streamer_refresh()
|
|
337
335
|
|
|
338
336
|
async def a_refresh(self) -> None:
|
|
@@ -357,14 +355,14 @@ class Session:
|
|
|
357
355
|
# update the relevant tokens
|
|
358
356
|
self.session_token = data["access_token"]
|
|
359
357
|
token_lifetime: int = data.get("expires_in", 900)
|
|
360
|
-
self.session_expiration =
|
|
358
|
+
self.session_expiration = now_in_new_york() + timedelta(token_lifetime)
|
|
361
359
|
logger.debug(f"Refreshed token, expires in {token_lifetime}ms")
|
|
362
360
|
auth_headers = {"Authorization": f"Bearer {self.session_token}"}
|
|
363
361
|
# update the httpx clients with the new token
|
|
364
362
|
self.sync_client.headers.update(auth_headers)
|
|
365
363
|
self.async_client.headers.update(auth_headers)
|
|
366
364
|
# update the streamer token if necessary
|
|
367
|
-
if self.streamer_expiration < self.session_expiration:
|
|
365
|
+
if not self.is_test and self.streamer_expiration < self.session_expiration:
|
|
368
366
|
# Pull streamer tokens and urls
|
|
369
367
|
data = await self._a_get("/api-quote-tokens")
|
|
370
368
|
# Auth token for dxfeed websocket
|
|
@@ -378,8 +376,6 @@ class Session:
|
|
|
378
376
|
async def a_get_customer(self) -> Customer:
|
|
379
377
|
"""
|
|
380
378
|
Gets the customer dict from the API.
|
|
381
|
-
|
|
382
|
-
:return: a Tastytrade 'Customer' object in JSON format.
|
|
383
379
|
"""
|
|
384
380
|
data = await self._a_get("/customers/me")
|
|
385
381
|
return Customer(**data)
|
|
@@ -387,8 +383,6 @@ class Session:
|
|
|
387
383
|
def get_customer(self) -> Customer:
|
|
388
384
|
"""
|
|
389
385
|
Gets the customer dict from the API.
|
|
390
|
-
|
|
391
|
-
:return: a Tastytrade 'Customer' object in JSON format.
|
|
392
386
|
"""
|
|
393
387
|
data = self._get("/customers/me")
|
|
394
388
|
return Customer(**data)
|
|
@@ -396,8 +390,6 @@ class Session:
|
|
|
396
390
|
async def a_validate(self) -> bool:
|
|
397
391
|
"""
|
|
398
392
|
Validates the current session by sending a request to the API.
|
|
399
|
-
|
|
400
|
-
:return: True if the session is valid and False otherwise.
|
|
401
393
|
"""
|
|
402
394
|
response = await self.async_client.post("/sessions/validate")
|
|
403
395
|
return response.status_code // 100 == 2
|
|
@@ -405,8 +397,6 @@ class Session:
|
|
|
405
397
|
def validate(self) -> bool:
|
|
406
398
|
"""
|
|
407
399
|
Validates the current session by sending a request to the API.
|
|
408
|
-
|
|
409
|
-
:return: True if the session is valid and False otherwise.
|
|
410
400
|
"""
|
|
411
401
|
response = self.sync_client.post("/sessions/validate")
|
|
412
402
|
return response.status_code // 100 == 2
|
|
@@ -422,6 +412,7 @@ class Session:
|
|
|
422
412
|
del attrs["sync_client"]
|
|
423
413
|
attrs["session_expiration"] = self.session_expiration.strftime(_fmt)
|
|
424
414
|
attrs["streamer_expiration"] = self.streamer_expiration.strftime(_fmt)
|
|
415
|
+
attrs["headers"] = dict(self.sync_client.headers.copy())
|
|
425
416
|
return json.dumps(attrs)
|
|
426
417
|
|
|
427
418
|
@classmethod
|
|
@@ -429,17 +420,11 @@ class Session:
|
|
|
429
420
|
"""
|
|
430
421
|
Create a new Session object from a serialized string.
|
|
431
422
|
"""
|
|
432
|
-
deserialized = json.loads(serialized)
|
|
423
|
+
deserialized: dict[str, Any] = json.loads(serialized)
|
|
424
|
+
headers = deserialized.pop("headers")
|
|
433
425
|
self = cls.__new__(cls)
|
|
434
426
|
self.__dict__ = deserialized
|
|
435
427
|
base_url = CERT_URL if self.is_test else API_URL
|
|
436
|
-
headers = {
|
|
437
|
-
"Accept": "application/json",
|
|
438
|
-
"Content-Type": "application/json",
|
|
439
|
-
"Authorization": self.session_token
|
|
440
|
-
if "user" in deserialized
|
|
441
|
-
else f"Bearer {self.session_token}",
|
|
442
|
-
}
|
|
443
428
|
self.session_expiration = datetime.strptime(
|
|
444
429
|
deserialized["session_expiration"], _fmt
|
|
445
430
|
)
|
|
@@ -29,8 +29,6 @@ class PriceEffect(str, Enum):
|
|
|
29
29
|
def now_in_new_york() -> datetime:
|
|
30
30
|
"""
|
|
31
31
|
Gets the current time in the New York timezone.
|
|
32
|
-
|
|
33
|
-
:return: current time as datetime
|
|
34
32
|
"""
|
|
35
33
|
return datetime.now(TZ)
|
|
36
34
|
|
|
@@ -38,8 +36,6 @@ def now_in_new_york() -> datetime:
|
|
|
38
36
|
def today_in_new_york() -> date:
|
|
39
37
|
"""
|
|
40
38
|
Gets the current date in the New York timezone.
|
|
41
|
-
|
|
42
|
-
:return: current date
|
|
43
39
|
"""
|
|
44
40
|
return now_in_new_york().date()
|
|
45
41
|
|
|
@@ -50,8 +46,6 @@ def is_market_open_on(day: date | None = None) -> bool:
|
|
|
50
46
|
during the given day.
|
|
51
47
|
|
|
52
48
|
:param day: date to check. If not provided defaults to current NY date.
|
|
53
|
-
|
|
54
|
-
:return: whether the market opens on given day
|
|
55
49
|
"""
|
|
56
50
|
day = day or today_in_new_york()
|
|
57
51
|
date_range = NYSE.valid_days(day, day)
|
|
@@ -64,8 +58,6 @@ def get_third_friday(day: date | None = None) -> date:
|
|
|
64
58
|
or the monthly expiration associated with today's month.
|
|
65
59
|
|
|
66
60
|
:param day: date to check. If not provided defaults to current NY date.
|
|
67
|
-
|
|
68
|
-
:return: the associated monthly
|
|
69
61
|
"""
|
|
70
62
|
day = (day or today_in_new_york()).replace(day=1) + timedelta(weeks=2)
|
|
71
63
|
while day.weekday() != 4: # Friday
|
|
@@ -76,8 +68,6 @@ def get_third_friday(day: date | None = None) -> date:
|
|
|
76
68
|
def get_tasty_monthly() -> date:
|
|
77
69
|
"""
|
|
78
70
|
Gets the monthly expiration closest to 45 days from the current date.
|
|
79
|
-
|
|
80
|
-
:return: the closest to 45 DTE monthly expiration
|
|
81
71
|
"""
|
|
82
72
|
day = today_in_new_york()
|
|
83
73
|
exp1 = get_third_friday(day + timedelta(weeks=4))
|
|
@@ -101,8 +91,6 @@ def get_future_fx_monthly(day: date | None = None) -> date:
|
|
|
101
91
|
Wednesday.
|
|
102
92
|
|
|
103
93
|
:param day: date to check. If not provided defaults to current NY date.
|
|
104
|
-
|
|
105
|
-
:return: the associated monthly
|
|
106
94
|
"""
|
|
107
95
|
day = (day or today_in_new_york()).replace(day=1) + timedelta(weeks=1)
|
|
108
96
|
while day.weekday() != 2: # Wednesday
|
|
@@ -120,8 +108,6 @@ def get_future_treasury_monthly(day: date | None = None) -> date:
|
|
|
120
108
|
business day prior.
|
|
121
109
|
|
|
122
110
|
:param day: date to check. If not provided defaults to current NY date.
|
|
123
|
-
|
|
124
|
-
:return: the associated monthly
|
|
125
111
|
"""
|
|
126
112
|
day = day or today_in_new_york()
|
|
127
113
|
last_day = _get_last_day_of_month(day)
|
|
@@ -143,8 +129,6 @@ def get_future_metal_monthly(day: date | None = None) -> date:
|
|
|
143
129
|
which case they expire on the prior business day.
|
|
144
130
|
|
|
145
131
|
:param day: date to check. If not provided defaults to current NY date.
|
|
146
|
-
|
|
147
|
-
:return: the associated monthly
|
|
148
132
|
"""
|
|
149
133
|
day = day or today_in_new_york()
|
|
150
134
|
last_day = _get_last_day_of_month(day)
|
|
@@ -164,8 +148,6 @@ def get_future_grain_monthly(day: date | None = None) -> date:
|
|
|
164
148
|
least 2 business days, the last business day of the month.
|
|
165
149
|
|
|
166
150
|
:param day: date to check. If not provided defaults to current NY date.
|
|
167
|
-
|
|
168
|
-
:return: the associated monthly
|
|
169
151
|
"""
|
|
170
152
|
day = day or today_in_new_york()
|
|
171
153
|
last_day = _get_last_day_of_month(day)
|
|
@@ -185,8 +167,6 @@ def get_future_oil_monthly(day: date | None = None) -> date:
|
|
|
185
167
|
they expire 7 business days prior to the 25th day of the month.
|
|
186
168
|
|
|
187
169
|
:param day: date to check. If not provided defaults to current NY date.
|
|
188
|
-
|
|
189
|
-
:return: the associated monthly
|
|
190
170
|
"""
|
|
191
171
|
last_day = (day or today_in_new_york()).replace(day=25)
|
|
192
172
|
first_day = last_day.replace(day=1)
|
|
@@ -201,8 +181,6 @@ def get_future_index_monthly(day: date | None = None) -> date:
|
|
|
201
181
|
month.
|
|
202
182
|
|
|
203
183
|
:param day: date to check. If not provided defaults to current NY date.
|
|
204
|
-
|
|
205
|
-
:return: the associated monthly
|
|
206
184
|
"""
|
|
207
185
|
day = day or today_in_new_york()
|
|
208
186
|
last_day = _get_last_day_of_month(day)
|
|
@@ -224,8 +202,6 @@ def _dasherize(s: str) -> str:
|
|
|
224
202
|
Converts a string from snake case to dasherized.
|
|
225
203
|
|
|
226
204
|
:param s: string to convert
|
|
227
|
-
|
|
228
|
-
:return: dasherized string
|
|
229
205
|
"""
|
|
230
206
|
return s.replace("_", "-")
|
|
231
207
|
|
|
@@ -1368,15 +1368,15 @@ wheels = [
|
|
|
1368
1368
|
|
|
1369
1369
|
[[package]]
|
|
1370
1370
|
name = "pyright"
|
|
1371
|
-
version = "1.1.
|
|
1371
|
+
version = "1.1.407"
|
|
1372
1372
|
source = { registry = "https://pypi.org/simple" }
|
|
1373
1373
|
dependencies = [
|
|
1374
1374
|
{ name = "nodeenv" },
|
|
1375
1375
|
{ name = "typing-extensions" },
|
|
1376
1376
|
]
|
|
1377
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1377
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" }
|
|
1378
1378
|
wheels = [
|
|
1379
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1379
|
+
{ url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" },
|
|
1380
1380
|
]
|
|
1381
1381
|
|
|
1382
1382
|
[[package]]
|
|
@@ -1549,6 +1549,8 @@ wheels = [
|
|
|
1549
1549
|
{ url = "https://files.pythonhosted.org/packages/42/cd/85b422d24ee2096eaf6faa360c95ef9bdb59097d19b9624cebce4dd9bc2a/ruamel.yaml.clib-0.2.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:808c7190a0fe7ae7014c42f73897cf8e9ef14ff3aa533450e51b1e72ec5239ad", size = 725028, upload-time = "2025-09-22T19:51:19.782Z" },
|
|
1550
1550
|
{ url = "https://files.pythonhosted.org/packages/4d/ac/99e6e0ea2584f84f447069d0187fe411e9b5deb7e3ddecda25001cfc7a95/ruamel.yaml.clib-0.2.14-cp39-cp39-win32.whl", hash = "sha256:6d5472f63a31b042aadf5ed28dd3ef0523da49ac17f0463e10fda9c4a2773352", size = 100915, upload-time = "2025-09-22T19:51:21.764Z" },
|
|
1551
1551
|
{ url = "https://files.pythonhosted.org/packages/5d/8d/846e43369658958c99d959bb7774136fff9210f9017d91a4277818ceafbf/ruamel.yaml.clib-0.2.14-cp39-cp39-win_amd64.whl", hash = "sha256:8dd3c2cc49caa7a8d64b67146462aed6723a0495e44bf0aa0a2e94beaa8432f6", size = 118706, upload-time = "2025-09-22T19:51:20.878Z" },
|
|
1552
|
+
{ url = "https://files.pythonhosted.org/packages/e7/cd/150fdb96b8fab27fe08d8a59fe67554568727981806e6bc2677a16081ec7/ruamel_yaml_clib-0.2.14-cp314-cp314-win32.whl", hash = "sha256:9b4104bf43ca0cd4e6f738cb86326a3b2f6eef00f417bd1e7efb7bdffe74c539", size = 102394, upload-time = "2025-11-14T21:57:36.703Z" },
|
|
1553
|
+
{ url = "https://files.pythonhosted.org/packages/bd/e6/a3fa40084558c7e1dc9546385f22a93949c890a8b2e445b2ba43935f51da/ruamel_yaml_clib-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:13997d7d354a9890ea1ec5937a219817464e5cc344805b37671562a401ca3008", size = 122673, upload-time = "2025-11-14T21:57:38.177Z" },
|
|
1552
1554
|
]
|
|
1553
1555
|
|
|
1554
1556
|
[[package]]
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import webbrowser
|
|
3
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
4
|
-
from urllib.parse import parse_qs
|
|
5
|
-
|
|
6
|
-
import httpx
|
|
7
|
-
|
|
8
|
-
PORT = 8000
|
|
9
|
-
REDIRECT_URI = f"http://localhost:{PORT}"
|
|
10
|
-
SCOPES = ["read", "trade", "openid"]
|
|
11
|
-
|
|
12
|
-
authorize_url = "https://my.tastytrade.com/auth.html"
|
|
13
|
-
token_url = "https://api.tastyworks.com/oauth/token"
|
|
14
|
-
client_id = ""
|
|
15
|
-
client_secret = ""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
root_page = """
|
|
19
|
-
<!DOCTYPE html>
|
|
20
|
-
<html lang="en-us">
|
|
21
|
-
<head>
|
|
22
|
-
<meta charset="utf-8">
|
|
23
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
24
|
-
<title>OAuth Setup</title>
|
|
25
|
-
<!-- Favicon -->
|
|
26
|
-
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
|
27
|
-
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
|
28
|
-
rel="stylesheet"
|
|
29
|
-
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
|
30
|
-
crossorigin="anonymous">
|
|
31
|
-
</head>
|
|
32
|
-
<body>
|
|
33
|
-
<div class="container position-absolute top-50 start-50 translate-middle"
|
|
34
|
-
style="width: 400px">
|
|
35
|
-
<form method="POST">
|
|
36
|
-
<div class="row mb-3">
|
|
37
|
-
<input type="text"
|
|
38
|
-
required
|
|
39
|
-
placeholder="Client ID"
|
|
40
|
-
name="client_id"
|
|
41
|
-
class="form-control">
|
|
42
|
-
</div>
|
|
43
|
-
<div class="row mb-3">
|
|
44
|
-
<input type="password"
|
|
45
|
-
required
|
|
46
|
-
placeholder="Client Secret"
|
|
47
|
-
name="client_secret"
|
|
48
|
-
class="form-control">
|
|
49
|
-
</div>
|
|
50
|
-
<div class="row mb-3">
|
|
51
|
-
<button type="submit" class="btn btn-success">Connect</button>
|
|
52
|
-
</div>
|
|
53
|
-
</form>
|
|
54
|
-
</div>
|
|
55
|
-
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
|
56
|
-
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
|
57
|
-
crossorigin="anonymous"></script>
|
|
58
|
-
</body>
|
|
59
|
-
</html>
|
|
60
|
-
""".encode()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class RequestHandler(BaseHTTPRequestHandler):
|
|
64
|
-
def do_POST(self) -> None:
|
|
65
|
-
global client_id, client_secret
|
|
66
|
-
content_length = int(self.headers["Content-Length"])
|
|
67
|
-
raw = self.rfile.read(content_length)
|
|
68
|
-
data = parse_qs(raw.decode("utf-8"))
|
|
69
|
-
client_id = data["client_id"][0]
|
|
70
|
-
client_secret = data["client_secret"][0]
|
|
71
|
-
|
|
72
|
-
# Redirect to login page using API key submitted by user
|
|
73
|
-
self.send_response(302)
|
|
74
|
-
query_string = "&".join(
|
|
75
|
-
[
|
|
76
|
-
"response_type=code",
|
|
77
|
-
f"redirect_uri={REDIRECT_URI}",
|
|
78
|
-
f"client_id={data['client_id'][0]}",
|
|
79
|
-
f"scope={' '.join(SCOPES)}",
|
|
80
|
-
]
|
|
81
|
-
)
|
|
82
|
-
url = f"{authorize_url}?{query_string}"
|
|
83
|
-
self.send_header("Location", url)
|
|
84
|
-
self.end_headers()
|
|
85
|
-
|
|
86
|
-
def do_GET(self) -> None:
|
|
87
|
-
global client_id, client_secret
|
|
88
|
-
# Serve root page with sign in link
|
|
89
|
-
if self.path == "/":
|
|
90
|
-
self.send_response(200)
|
|
91
|
-
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
92
|
-
self.end_headers()
|
|
93
|
-
self.wfile.write(root_page)
|
|
94
|
-
else:
|
|
95
|
-
# Check if query path contains case insensitive "code="
|
|
96
|
-
code_match = re.search(r"code=(.+)", self.path, re.I)
|
|
97
|
-
if code_match and client_id and client_secret:
|
|
98
|
-
user_auth_code = code_match[1]
|
|
99
|
-
post_data = {
|
|
100
|
-
"grant_type": "authorization_code",
|
|
101
|
-
"client_id": client_id,
|
|
102
|
-
"client_secret": client_secret,
|
|
103
|
-
"redirect_uri": REDIRECT_URI,
|
|
104
|
-
"code": user_auth_code,
|
|
105
|
-
}
|
|
106
|
-
response = httpx.post(token_url, data=post_data)
|
|
107
|
-
token_access = response.json()
|
|
108
|
-
refresh_token: str = token_access["refresh_token"]
|
|
109
|
-
print(refresh_token)
|
|
110
|
-
|
|
111
|
-
self.send_response(200)
|
|
112
|
-
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
113
|
-
self.end_headers()
|
|
114
|
-
self.wfile.write(refresh_token.encode())
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def login(is_test: bool = False) -> None:
|
|
118
|
-
"""
|
|
119
|
-
Starts a local HTTP server and opens the browser to OAuth login.
|
|
120
|
-
Designed for one-time use to get a refresh token.
|
|
121
|
-
"""
|
|
122
|
-
global authorize_url, token_url
|
|
123
|
-
if is_test:
|
|
124
|
-
authorize_url = "https://cert-my.staging-tasty.works/auth.html"
|
|
125
|
-
token_url = "https://api.cert.tastyworks.com/oauth/token"
|
|
126
|
-
httpd = HTTPServer(("", PORT), RequestHandler)
|
|
127
|
-
print(f"Opening url: {REDIRECT_URI}")
|
|
128
|
-
webbrowser.open(REDIRECT_URI)
|
|
129
|
-
httpd.serve_forever()
|
tastytrade-11.0.3/tmp.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
from datetime import date, datetime, time
|
|
4
|
-
|
|
5
|
-
import pandas as pd
|
|
6
|
-
|
|
7
|
-
from tastytrade import Session
|
|
8
|
-
from tastytrade.dxfeed.candle import Candle
|
|
9
|
-
from tastytrade.streamer import DXLinkStreamer
|
|
10
|
-
from tastytrade.utils import TZ
|
|
11
|
-
|
|
12
|
-
session = Session(os.environ["TT_SECRET"], os.environ["TT_REFRESH"])
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
async def main():
|
|
16
|
-
async with DXLinkStreamer(session) as streamer:
|
|
17
|
-
start_time = datetime.combine(date(2025, 11, 7), time(9, 30), tzinfo=TZ)
|
|
18
|
-
ts = round(start_time.timestamp() * 1000)
|
|
19
|
-
await streamer.subscribe_candle(["DIA"], "5s", start_time=start_time)
|
|
20
|
-
candles: list[Candle] = []
|
|
21
|
-
async for candle in streamer.listen(Candle):
|
|
22
|
-
candles.append(candle)
|
|
23
|
-
if candle.time <= ts:
|
|
24
|
-
break
|
|
25
|
-
candles.sort(key=lambda c: c.time)
|
|
26
|
-
df = pd.DataFrame([c.model_dump() for c in candles])
|
|
27
|
-
df = df[["time", "open", "high", "low", "close"]]
|
|
28
|
-
print(df)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if __name__ == "__main__":
|
|
32
|
-
asyncio.run(main())
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|