tastytrade 11.0.2__py3-none-any.whl → 11.0.4__py3-none-any.whl
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/__init__.py +1 -1
- tastytrade/order.py +6 -2
- tastytrade/session.py +9 -11
- {tastytrade-11.0.2.dist-info → tastytrade-11.0.4.dist-info}/METADATA +1 -1
- {tastytrade-11.0.2.dist-info → tastytrade-11.0.4.dist-info}/RECORD +7 -8
- {tastytrade-11.0.2.dist-info → tastytrade-11.0.4.dist-info}/WHEEL +1 -1
- tastytrade/oauth.py +0 -129
- {tastytrade-11.0.2.dist-info → tastytrade-11.0.4.dist-info}/licenses/LICENSE +0 -0
tastytrade/__init__.py
CHANGED
|
@@ -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.4"
|
|
8
8
|
|
|
9
9
|
__version__ = VERSION
|
|
10
10
|
version_str: str = f"tastyware/tastytrade:v{VERSION}"
|
tastytrade/order.py
CHANGED
|
@@ -254,6 +254,8 @@ class NewOrder(TastytradeData):
|
|
|
254
254
|
preflight_id: str | None = None
|
|
255
255
|
rules: OrderRule | None = None
|
|
256
256
|
advanced_instructions: AdvancedInstructions | None = None
|
|
257
|
+
#: External identifier for the order, used to track orders across systems
|
|
258
|
+
external_identifier: str | None = None
|
|
257
259
|
|
|
258
260
|
@computed_field # type: ignore[misc]
|
|
259
261
|
@property
|
|
@@ -316,8 +318,8 @@ class PlacedOrder(TastytradeData):
|
|
|
316
318
|
cancelled_at: datetime | None = None
|
|
317
319
|
cancel_user_id: str | None = None
|
|
318
320
|
cancel_username: str | None = None
|
|
319
|
-
replacing_order_id:
|
|
320
|
-
replaces_order_id:
|
|
321
|
+
replacing_order_id: int | None = None
|
|
322
|
+
replaces_order_id: int | None = None
|
|
321
323
|
in_flight_at: datetime | None = None
|
|
322
324
|
live_at: datetime | None = None
|
|
323
325
|
received_at: datetime | None = None
|
|
@@ -330,6 +332,8 @@ class PlacedOrder(TastytradeData):
|
|
|
330
332
|
preflight_id: str | int | None = None
|
|
331
333
|
order_rule: OrderRule | None = None
|
|
332
334
|
source: str | None = None
|
|
335
|
+
#: External identifier for the order, used to track orders across systems
|
|
336
|
+
external_identifier: str | None = None
|
|
333
337
|
|
|
334
338
|
@model_validator(mode="before")
|
|
335
339
|
@classmethod
|
tastytrade/session.py
CHANGED
|
@@ -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
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
tastytrade/__init__.py,sha256=
|
|
1
|
+
tastytrade/__init__.py,sha256=pwEAcH8BTcujK6-su08wNhuTUNsfta3vDpTSdnQaLBY,531
|
|
2
2
|
tastytrade/account.py,sha256=EKRx2Yta4TjPB8U2DuIqiOZ_pdgBUXyeTMaGdNeVRwU,56859
|
|
3
3
|
tastytrade/instruments.py,sha256=kH2AuAu6UaRwj7Uz1dYrLe6rFkJqEMXJ1Lg_cMyPg_E,46789
|
|
4
4
|
tastytrade/market_data.py,sha256=iyBpleKvDWfXReo6DuKbXP-gN1Yv8iyvNs9z2x5Gxig,5940
|
|
5
5
|
tastytrade/market_sessions.py,sha256=r0L-4CSzjx2gC3pJS1C0ae9RiahaKWs67Ljng3DwODI,3382
|
|
6
6
|
tastytrade/metrics.py,sha256=LqpUZIR4L2bQwawf_L4gJ3Pjg_S72eURrJpu4ebKtpU,7495
|
|
7
|
-
tastytrade/
|
|
8
|
-
tastytrade/order.py,sha256=NYl2YZSHnX02WXU-ihBtIPhOByQ_6uH__ls7gm5Ikrk,14868
|
|
7
|
+
tastytrade/order.py,sha256=ybhy3Ha2RLqqBRxt4ygpSnUVdbT5H-OOJ6QEaj0id34,15110
|
|
9
8
|
tastytrade/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
9
|
tastytrade/search.py,sha256=LdoVEhiYNvtolXlf_jAVZUtA2ymUWOvHMhQxJxuVt_A,1529
|
|
11
|
-
tastytrade/session.py,sha256=
|
|
10
|
+
tastytrade/session.py,sha256=scAJkldV8gWotOWWFxA4BSnuLdW2HfWt9PiQO_KDZ6g,15516
|
|
12
11
|
tastytrade/streamer.py,sha256=7HISXzo3AKloOafMDp5NHSxW19cRcobfrrOUoymt-g4,32609
|
|
13
12
|
tastytrade/utils.py,sha256=bLKzR6L3QqEQB_SMXgSCKqTELvdkeelu8ksycImIkKI,12766
|
|
14
13
|
tastytrade/watchlists.py,sha256=K9jpgXi9a9dklGl-jCXzpbM0tOd836GSkXa4fySeH3Q,8657
|
|
@@ -23,7 +22,7 @@ tastytrade/dxfeed/theoprice.py,sha256=L5aH--F_6xLZCSYZ4APpzlihbW0-cYEwRdeGVI-aNa
|
|
|
23
22
|
tastytrade/dxfeed/timeandsale.py,sha256=QuMFoccq8x3c2y6s3DnwBNIVTrLS6OPqV6GmCNoXQEQ,1903
|
|
24
23
|
tastytrade/dxfeed/trade.py,sha256=qNo4oKb7iq0Opoq3FCBEUUcGGF6udda1bD0eKQVty_0,1402
|
|
25
24
|
tastytrade/dxfeed/underlying.py,sha256=YYqJNlmrlt6Kpg0F6voQ18g60obXiYTVlroXirBWPR8,1226
|
|
26
|
-
tastytrade-11.0.
|
|
27
|
-
tastytrade-11.0.
|
|
28
|
-
tastytrade-11.0.
|
|
29
|
-
tastytrade-11.0.
|
|
25
|
+
tastytrade-11.0.4.dist-info/METADATA,sha256=XHOY6saD3h8mJWFtDbirg3lLMWoR9FqcU5O4fxcVAEM,10904
|
|
26
|
+
tastytrade-11.0.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
27
|
+
tastytrade-11.0.4.dist-info/licenses/LICENSE,sha256=enBkMN4OsfLt6Z_AsrGC7u5dAJkCEODnoN7BwMCzSfc,1072
|
|
28
|
+
tastytrade-11.0.4.dist-info/RECORD,,
|
tastytrade/oauth.py
DELETED
|
@@ -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()
|
|
File without changes
|