schwabdev 2.2.2__tar.gz → 2.2.4__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.
- {schwabdev-2.2.2 → schwabdev-2.2.4}/PKG-INFO +7 -5
- {schwabdev-2.2.2 → schwabdev-2.2.4}/README.md +6 -4
- schwabdev-2.2.4/pyproject.toml +3 -0
- {schwabdev-2.2.2 → schwabdev-2.2.4}/schwabdev/api.py +82 -68
- {schwabdev-2.2.2 → schwabdev-2.2.4}/schwabdev/stream.py +89 -65
- {schwabdev-2.2.2 → schwabdev-2.2.4}/schwabdev.egg-info/PKG-INFO +7 -5
- {schwabdev-2.2.2 → schwabdev-2.2.4}/schwabdev.egg-info/SOURCES.txt +1 -0
- {schwabdev-2.2.2 → schwabdev-2.2.4}/setup.py +1 -1
- {schwabdev-2.2.2 → schwabdev-2.2.4}/LICENSE.txt +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.4}/schwabdev/__init__.py +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.4}/schwabdev.egg-info/dependency_links.txt +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.4}/schwabdev.egg-info/requires.txt +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.4}/schwabdev.egg-info/top_level.txt +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: schwabdev
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: An easy and lightweight wrapper for using the Charles Schwab API.
|
|
5
5
|
Author: Tyler Bowers
|
|
6
6
|
Author-email: tylerebowers@gmail.com
|
|
@@ -28,20 +28,22 @@ This is an unofficial python program to access the Schwab api.
|
|
|
28
28
|
[Discord](https://discord.gg/m7SSjr9rs9), [PyPI](https://pypi.org/project/schwabdev/), [Youtube](https://youtube.com/playlist?list=PLs4JLWxBQIxpbvCj__DjAc0RRTlBz-TR8), [Github](https://github.com/tylerebowers/Schwab-API-Python).
|
|
29
29
|
|
|
30
30
|
## Installation
|
|
31
|
-
`pip install schwabdev
|
|
31
|
+
`pip install schwabdev`
|
|
32
32
|
*You may need to use `pip3` instead of `pip`*
|
|
33
33
|
|
|
34
34
|
## Quick setup
|
|
35
35
|
1. Setup your Schwab developer account [here](https://beta-developer.schwab.com/).
|
|
36
36
|
- Create a new Schwab individual developer app with callback url "https://127.0.0.1" (case sensitive)
|
|
37
|
+
- Add both API products to your app: "Accounts and Trading Production" and "Market Data Production".
|
|
37
38
|
- Wait until the status is "Ready for use", note that "Approved - Pending" will not work.
|
|
38
39
|
- Enable TOS (Thinkorswim) for your Schwab account, it is needed for orders and other api calls.
|
|
39
40
|
2. Install packages
|
|
40
|
-
- Install schwabdev and requirements `pip install schwabdev
|
|
41
|
+
- Install schwabdev and requirements `pip install schwabdev`
|
|
41
42
|
- *You may need to use `pip3` instead of `pip`*
|
|
42
43
|
3. Examples on how to use the client are in the `examples/` folder (add your keys in the .env file)
|
|
43
|
-
- The first time you run you will have to sign in to your Schwab account using the generated link in the terminal.
|
|
44
|
-
-
|
|
44
|
+
- The first time you run you will have to sign in to your Schwab account using the generated link in the terminal.
|
|
45
|
+
- After signing in, agree to the terms, and select account(s). Then you will have to copy the link in the address bar and paste it into the terminal.
|
|
46
|
+
- Questions? - join the [Discord group](https://discord.gg/m7SSjr9rs9) or consult the `/docs` folder.
|
|
45
47
|
```py
|
|
46
48
|
import schwabdev #import the package
|
|
47
49
|
|
|
@@ -4,20 +4,22 @@ This is an unofficial python program to access the Schwab api.
|
|
|
4
4
|
[Discord](https://discord.gg/m7SSjr9rs9), [PyPI](https://pypi.org/project/schwabdev/), [Youtube](https://youtube.com/playlist?list=PLs4JLWxBQIxpbvCj__DjAc0RRTlBz-TR8), [Github](https://github.com/tylerebowers/Schwab-API-Python).
|
|
5
5
|
|
|
6
6
|
## Installation
|
|
7
|
-
`pip install schwabdev
|
|
7
|
+
`pip install schwabdev`
|
|
8
8
|
*You may need to use `pip3` instead of `pip`*
|
|
9
9
|
|
|
10
10
|
## Quick setup
|
|
11
11
|
1. Setup your Schwab developer account [here](https://beta-developer.schwab.com/).
|
|
12
12
|
- Create a new Schwab individual developer app with callback url "https://127.0.0.1" (case sensitive)
|
|
13
|
+
- Add both API products to your app: "Accounts and Trading Production" and "Market Data Production".
|
|
13
14
|
- Wait until the status is "Ready for use", note that "Approved - Pending" will not work.
|
|
14
15
|
- Enable TOS (Thinkorswim) for your Schwab account, it is needed for orders and other api calls.
|
|
15
16
|
2. Install packages
|
|
16
|
-
- Install schwabdev and requirements `pip install schwabdev
|
|
17
|
+
- Install schwabdev and requirements `pip install schwabdev`
|
|
17
18
|
- *You may need to use `pip3` instead of `pip`*
|
|
18
19
|
3. Examples on how to use the client are in the `examples/` folder (add your keys in the .env file)
|
|
19
|
-
- The first time you run you will have to sign in to your Schwab account using the generated link in the terminal.
|
|
20
|
-
-
|
|
20
|
+
- The first time you run you will have to sign in to your Schwab account using the generated link in the terminal.
|
|
21
|
+
- After signing in, agree to the terms, and select account(s). Then you will have to copy the link in the address bar and paste it into the terminal.
|
|
22
|
+
- Questions? - join the [Discord group](https://discord.gg/m7SSjr9rs9) or consult the `/docs` folder.
|
|
21
23
|
```py
|
|
22
24
|
import schwabdev #import the package
|
|
23
25
|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file contains functions access the Schwab api
|
|
3
|
+
Coded by Tyler Bowers
|
|
4
|
+
Github: https://github.com/tylerebowers/Schwab-API-Python
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
import json
|
|
2
8
|
import time
|
|
3
9
|
import base64
|
|
@@ -6,6 +12,7 @@ import requests
|
|
|
6
12
|
import threading
|
|
7
13
|
import webbrowser
|
|
8
14
|
import urllib.parse
|
|
15
|
+
|
|
9
16
|
from .stream import Stream
|
|
10
17
|
|
|
11
18
|
class Client:
|
|
@@ -48,21 +55,22 @@ class Client:
|
|
|
48
55
|
elif timeout <= 0:
|
|
49
56
|
raise Exception("Timeout must be greater than 0 and is recomended to be 5 seconds or more.")
|
|
50
57
|
|
|
51
|
-
self.
|
|
52
|
-
self.
|
|
53
|
-
self.
|
|
54
|
-
self.
|
|
55
|
-
self.
|
|
56
|
-
self.
|
|
57
|
-
self.
|
|
58
|
-
self.
|
|
59
|
-
self.
|
|
60
|
-
self.
|
|
61
|
-
self.
|
|
62
|
-
self.
|
|
63
|
-
self.
|
|
64
|
-
self.
|
|
65
|
-
self.
|
|
58
|
+
self.version = "2.2.4"
|
|
59
|
+
self._app_key = app_key # app key credential
|
|
60
|
+
self._app_secret = app_secret # app secret credential
|
|
61
|
+
self._callback_url = callback_url # callback url to use
|
|
62
|
+
self.access_token = None # access token from auth
|
|
63
|
+
self.refresh_token = None # refresh token from auth
|
|
64
|
+
self.id_token = None # id token from auth
|
|
65
|
+
self._access_token_issued = None # datetime of access token issue
|
|
66
|
+
self._refresh_token_issued = None # datetime of refresh token issue
|
|
67
|
+
self._access_token_timeout = 1800 # in seconds (from schwab)
|
|
68
|
+
self._refresh_token_timeout = 7*24*60*60 # in seconds (from schwab)
|
|
69
|
+
self._tokens_file = tokens_file # path to tokens file
|
|
70
|
+
self.timeout = timeout # timeout to use in requests
|
|
71
|
+
self.verbose = verbose # verbose mode
|
|
72
|
+
self.stream = Stream(self) # init the streaming object
|
|
73
|
+
self.awaiting_input = False # whether we are awaiting user input
|
|
66
74
|
|
|
67
75
|
# Try to load tokens from the tokens file
|
|
68
76
|
at_issued, rt_issued, token_dictionary = self._read_tokens_file()
|
|
@@ -74,14 +82,16 @@ class Client:
|
|
|
74
82
|
self._access_token_issued = at_issued
|
|
75
83
|
self._refresh_token_issued = rt_issued
|
|
76
84
|
if self.verbose:
|
|
77
|
-
|
|
78
|
-
print(
|
|
85
|
+
at_delta = self._access_token_timeout - (datetime.datetime.now(datetime.timezone.utc) - self._access_token_issued).total_seconds()
|
|
86
|
+
print(f"[Schwabdev] Access token expires in {"-" if at_delta < 0 else ""}{int(abs(at_delta) / 3600)}:{int((abs(at_delta) % 3600) / 60)}:{int((abs(at_delta) % 60))}")
|
|
87
|
+
rt_delta = self._refresh_token_timeout - (datetime.datetime.now(datetime.timezone.utc) - self._refresh_token_issued).total_seconds()
|
|
88
|
+
print(f"[Schwabdev] Refresh token expires in {"-" if rt_delta < 0 else ""}{int(abs(rt_delta) / 3600)}:{int((abs(rt_delta) % 3600) / 60)}:{int((abs(rt_delta) % 60))}")
|
|
79
89
|
# check if tokens need to be updated and update if needed
|
|
80
90
|
self.update_tokens()
|
|
81
91
|
else:
|
|
82
92
|
# The tokens file doesn't exist, so create it.
|
|
83
93
|
if self.verbose:
|
|
84
|
-
print(f"Token file does not exist or invalid formatting, creating \"{str(tokens_file)}\"")
|
|
94
|
+
print(f"[Schwabdev] Token file does not exist or invalid formatting, creating \"{str(tokens_file)}\"")
|
|
85
95
|
open(self._tokens_file, 'w').close()
|
|
86
96
|
# Tokens must be updated.
|
|
87
97
|
self._update_refresh_token()
|
|
@@ -91,13 +101,13 @@ class Client:
|
|
|
91
101
|
def checker():
|
|
92
102
|
while True:
|
|
93
103
|
self.update_tokens()
|
|
94
|
-
time.sleep(
|
|
104
|
+
time.sleep(30)
|
|
95
105
|
threading.Thread(target=checker, daemon=True).start()
|
|
96
|
-
elif
|
|
97
|
-
print("Warning: Tokens will not be updated automatically.")
|
|
106
|
+
elif self.verbose:
|
|
107
|
+
print("[Schwabdev] Warning: Tokens will not be updated automatically.")
|
|
98
108
|
|
|
99
109
|
if self.verbose:
|
|
100
|
-
print("Schwabdev Client Initialization Complete")
|
|
110
|
+
print("[Schwabdev] Client Initialization Complete")
|
|
101
111
|
|
|
102
112
|
def update_tokens(self, force=False):
|
|
103
113
|
"""
|
|
@@ -105,17 +115,21 @@ class Client:
|
|
|
105
115
|
:param force: force update of refresh token (also updates access token)
|
|
106
116
|
:type force: bool
|
|
107
117
|
"""
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
#refresh token notification.
|
|
119
|
+
rt_delta = self._refresh_token_timeout - (datetime.datetime.now(datetime.timezone.utc) - self._refresh_token_issued).total_seconds()
|
|
120
|
+
if rt_delta < 43200: # Start to ware the user if the refresh token will expire in less than 43200 = 12 hours
|
|
121
|
+
print(f"[Schwabdev] The refresh token will expire soon! ({"-" if rt_delta < 0 else ""}{int(abs(rt_delta) / 3600)}:{int((abs(rt_delta) % 3600) / 60)}:{int((abs(rt_delta) % 60))} remaining)")
|
|
122
|
+
|
|
123
|
+
if (rt_delta < 3600) or force: # check if we need to update refresh (and access) token
|
|
124
|
+
print("[Schwabdev] The refresh token has expired!")
|
|
110
125
|
self._update_refresh_token()
|
|
111
|
-
elif ((datetime.datetime.now(datetime.timezone.utc) - self._access_token_issued).
|
|
112
|
-
|
|
113
|
-
if self.verbose: print("The access token has expired, updating automatically.")
|
|
126
|
+
elif (self._access_token_timeout - (datetime.datetime.now(datetime.timezone.utc) - self._access_token_issued).total_seconds()) < 61: # check if we need to update access token
|
|
127
|
+
if self.verbose: print("[Schwabdev] The access token has expired, updating automatically.")
|
|
114
128
|
self._update_access_token()
|
|
115
129
|
|
|
116
130
|
def update_tokens_auto(self):
|
|
117
131
|
import warnings
|
|
118
|
-
warnings.warn("update_tokens_auto() is deprecated and is now started when the client is created (if update_tokens_auto=True (default)).", DeprecationWarning, stacklevel=2)
|
|
132
|
+
warnings.warn("update_tokens_auto() is deprecated and is now started by default when the client is created (if update_tokens_auto=True (default)).", DeprecationWarning, stacklevel=2)
|
|
119
133
|
|
|
120
134
|
def _update_access_token(self):
|
|
121
135
|
"""
|
|
@@ -136,11 +150,11 @@ class Client:
|
|
|
136
150
|
self.id_token = new_td.get("id_token")
|
|
137
151
|
self._write_tokens_file(self._access_token_issued, refresh_token_issued, new_td)
|
|
138
152
|
if self.verbose: # show user that we have updated the access token
|
|
139
|
-
print(f"Access token updated: {self._access_token_issued}")
|
|
153
|
+
print(f"[Schwabdev] Access token updated: {self._access_token_issued}")
|
|
140
154
|
break
|
|
141
155
|
else:
|
|
142
156
|
print(response.text)
|
|
143
|
-
print(f"Could not get new access token ({i+1} of 3).")
|
|
157
|
+
print(f"[Schwabdev] Could not get new access token ({i+1} of 3).")
|
|
144
158
|
time.sleep(10)
|
|
145
159
|
|
|
146
160
|
def _update_refresh_token(self):
|
|
@@ -149,9 +163,9 @@ class Client:
|
|
|
149
163
|
"""
|
|
150
164
|
self.awaiting_input = True # set flag since we are waiting for user input
|
|
151
165
|
# get authorization code (requires user to authorize)
|
|
152
|
-
#print("Please authorize this program to access your schwab account.")
|
|
166
|
+
#print("[Schwabdev] Please authorize this program to access your schwab account.")
|
|
153
167
|
auth_url = f'https://api.schwabapi.com/v1/oauth/authorize?client_id={self._app_key}&redirect_uri={self._callback_url}'
|
|
154
|
-
print(f"Open to authenticate: {auth_url}")
|
|
168
|
+
print(f"[Schwabdev] Open to authenticate: {auth_url}")
|
|
155
169
|
webbrowser.open(auth_url)
|
|
156
170
|
response_url = input("After authorizing, paste the address bar url here: ")
|
|
157
171
|
code = f"{response_url[response_url.index('code=') + 5:response_url.index('%40')]}@" # session = responseURL[responseURL.index("session=")+8:]
|
|
@@ -166,14 +180,14 @@ class Client:
|
|
|
166
180
|
self.awaiting_input = False # reset flag since tokens have been updated
|
|
167
181
|
self.id_token = new_td.get("id_token")
|
|
168
182
|
self._write_tokens_file(self._access_token_issued, self._refresh_token_issued, new_td)
|
|
169
|
-
if self.verbose: print("Refresh and Access tokens updated")
|
|
183
|
+
if self.verbose: print("[Schwabdev] Refresh and Access tokens updated")
|
|
170
184
|
else:
|
|
171
185
|
print(response.text)
|
|
172
|
-
print("Could not get new refresh and access tokens, check these:\n 1. App status is "
|
|
186
|
+
print("[Schwabdev] Could not get new refresh and access tokens, check these:\n 1. App status is "
|
|
173
187
|
"\"Ready For Use\".\n 2. App key and app secret are valid.\n 3. You pasted the "
|
|
174
188
|
"whole url within 30 seconds. (it has a quick expiration)")
|
|
175
189
|
|
|
176
|
-
def _post_oauth_token(self, grant_type, code):
|
|
190
|
+
def _post_oauth_token(self, grant_type: str, code: str):
|
|
177
191
|
"""
|
|
178
192
|
Makes API calls for auth code and refresh tokens
|
|
179
193
|
:param grant_type: 'authorization_code' or 'refresh_token'
|
|
@@ -195,7 +209,7 @@ class Client:
|
|
|
195
209
|
raise Exception("Invalid grant type; options are 'authorization_code' or 'refresh_token'")
|
|
196
210
|
return requests.post('https://api.schwabapi.com/v1/oauth/token', headers=headers, data=data)
|
|
197
211
|
|
|
198
|
-
def _write_tokens_file(self, at_issued, rt_issued, token_dictionary):
|
|
212
|
+
def _write_tokens_file(self, at_issued: datetime, rt_issued: datetime, token_dictionary: dict):
|
|
199
213
|
"""
|
|
200
214
|
Writes token file
|
|
201
215
|
:param at_issued: access token issued
|
|
@@ -229,7 +243,7 @@ class Client:
|
|
|
229
243
|
print(e)
|
|
230
244
|
return None, None, None
|
|
231
245
|
|
|
232
|
-
def _params_parser(self, params):
|
|
246
|
+
def _params_parser(self, params: dict):
|
|
233
247
|
"""
|
|
234
248
|
Removes None (null) values
|
|
235
249
|
:param params: params to remove None values from
|
|
@@ -241,11 +255,11 @@ class Client:
|
|
|
241
255
|
if params[key] is None: del params[key]
|
|
242
256
|
return params
|
|
243
257
|
|
|
244
|
-
def _time_convert(self, dt=None, form="8601"):
|
|
258
|
+
def _time_convert(self, dt = None, form="8601"):
|
|
245
259
|
"""
|
|
246
260
|
Convert time to the correct format, passthrough if a string, preserve None if None for params parser
|
|
247
261
|
:param dt: datetime.pyi object to convert
|
|
248
|
-
:type dt: datetime.pyi
|
|
262
|
+
:type dt: datetime.pyi | str | None
|
|
249
263
|
:param form: what to convert input to
|
|
250
264
|
:type form: str
|
|
251
265
|
:return: converted time or passthrough
|
|
@@ -295,11 +309,11 @@ class Client:
|
|
|
295
309
|
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
296
310
|
timeout=self.timeout)
|
|
297
311
|
|
|
298
|
-
def account_details_all(self, fields=None) -> requests.Response:
|
|
312
|
+
def account_details_all(self, fields: str = None) -> requests.Response:
|
|
299
313
|
"""
|
|
300
314
|
All the linked account information for the user logged in. The balances on these accounts are displayed by default however the positions on these accounts will be displayed based on the "positions" flag.
|
|
301
315
|
:param fields: fields to return (options: "positions")
|
|
302
|
-
:type fields: str
|
|
316
|
+
:type fields: str | None
|
|
303
317
|
:return: details for all linked accounts
|
|
304
318
|
:rtype: request.Response
|
|
305
319
|
"""
|
|
@@ -308,13 +322,13 @@ class Client:
|
|
|
308
322
|
params=self._params_parser({'fields': fields}),
|
|
309
323
|
timeout=self.timeout)
|
|
310
324
|
|
|
311
|
-
def account_details(self, accountHash: str, fields=None) -> requests.Response:
|
|
325
|
+
def account_details(self, accountHash: str, fields: str = None) -> requests.Response:
|
|
312
326
|
"""
|
|
313
327
|
Specific account information with balances and positions. The balance information on these accounts is displayed by default but Positions will be returned based on the "positions" flag.
|
|
314
328
|
:param accountHash: account hash from account_linked()
|
|
315
329
|
:type accountHash: str
|
|
316
330
|
:param fields: fields to return
|
|
317
|
-
:type fields: str
|
|
331
|
+
:type fields: str | None
|
|
318
332
|
:return: details for one linked account
|
|
319
333
|
:rtype: request.Response
|
|
320
334
|
"""
|
|
@@ -323,7 +337,7 @@ class Client:
|
|
|
323
337
|
params=self._params_parser({'fields': fields}),
|
|
324
338
|
timeout=self.timeout)
|
|
325
339
|
|
|
326
|
-
def account_orders(self, accountHash: str, fromEnteredTime:
|
|
340
|
+
def account_orders(self, accountHash: str, fromEnteredTime: datetime.datetime | str, toEnteredTime: datetime.datetime | str, maxResults: int = None, status: str = None) -> requests.Response:
|
|
327
341
|
"""
|
|
328
342
|
All orders for a specific account. Orders retrieved can be filtered based on input parameters below. Maximum date range is 1 year.
|
|
329
343
|
:param accountHash: account hash from account_linked()
|
|
@@ -333,9 +347,9 @@ class Client:
|
|
|
333
347
|
:param toEnteredTime: to entered time
|
|
334
348
|
:type toEnteredTime: datetime.pyi | str
|
|
335
349
|
:param maxResults: maximum number of results
|
|
336
|
-
:type maxResults: int
|
|
350
|
+
:type maxResults: int| None
|
|
337
351
|
:param status: status ("AWAITING_PARENT_ORDER"|"AWAITING_CONDITION"|"AWAITING_STOP_CONDITION"|"AWAITING_MANUAL_REVIEW"|"ACCEPTED"|"AWAITING_UR_OUT"|"PENDING_ACTIVATION"|"QUEUED"|"WORKING"|"REJECTED"|"PENDING_CANCEL"|"CANCELED"|"PENDING_REPLACE"|"REPLACED"|"FILLED"|"EXPIRED"|"NEW"|"AWAITING_RELEASE_TIME"|"PENDING_ACKNOWLEDGEMENT"|"PENDING_RECALL"|"UNKNOWN")
|
|
338
|
-
:type status: str
|
|
352
|
+
:type status: str| None
|
|
339
353
|
:return: orders for one linked account hash
|
|
340
354
|
:rtype: request.Response
|
|
341
355
|
"""
|
|
@@ -362,7 +376,7 @@ class Client:
|
|
|
362
376
|
json=order,
|
|
363
377
|
timeout=self.timeout)
|
|
364
378
|
|
|
365
|
-
def order_details(self, accountHash:str, orderId: int | str) -> requests.Response:
|
|
379
|
+
def order_details(self, accountHash: str, orderId: int | str) -> requests.Response:
|
|
366
380
|
"""
|
|
367
381
|
Get a specific order by its ID, for a specific account
|
|
368
382
|
:param accountHash: account hash from account_linked()
|
|
@@ -408,7 +422,7 @@ class Client:
|
|
|
408
422
|
json=order,
|
|
409
423
|
timeout=self.timeout)
|
|
410
424
|
|
|
411
|
-
def account_orders_all(self, fromEnteredTime:
|
|
425
|
+
def account_orders_all(self, fromEnteredTime: datetime.datetime | str, toEnteredTime: datetime.datetime | str, maxResults: int = None, status: str = None) -> requests.Response:
|
|
412
426
|
"""
|
|
413
427
|
Get all orders for all accounts
|
|
414
428
|
:param fromEnteredTime: start date
|
|
@@ -416,9 +430,9 @@ class Client:
|
|
|
416
430
|
:param toEnteredTime: end date
|
|
417
431
|
:type toEnteredTime: datetime.pyi | str
|
|
418
432
|
:param maxResults: maximum number of results (set to None for default 3000)
|
|
419
|
-
:type maxResults: int
|
|
433
|
+
:type maxResults: int | None
|
|
420
434
|
:param status: status ("AWAITING_PARENT_ORDER"|"AWAITING_CONDITION"|"AWAITING_STOP_CONDITION"|"AWAITING_MANUAL_REVIEW"|"ACCEPTED"|"AWAITING_UR_OUT"|"PENDING_ACTIVATION"|"QUEUED"|"WORKING"|"REJECTED"|"PENDING_CANCEL"|"CANCELED"|"PENDING_REPLACE"|"REPLACED"|"FILLED"|"EXPIRED"|"NEW"|"AWAITING_RELEASE_TIME"|"PENDING_ACKNOWLEDGEMENT"|"PENDING_RECALL"|"UNKNOWN")
|
|
421
|
-
:type status: str
|
|
435
|
+
:type status: str | None
|
|
422
436
|
:return: all orders
|
|
423
437
|
:rtype: request.Response
|
|
424
438
|
"""
|
|
@@ -437,7 +451,7 @@ class Client:
|
|
|
437
451
|
"Content-Type": "application.json"}, data=orderObject)
|
|
438
452
|
"""
|
|
439
453
|
|
|
440
|
-
def transactions(self, accountHash: str, startDate:
|
|
454
|
+
def transactions(self, accountHash: str, startDate: datetime.datetime | str, endDate: datetime.datetime | str, types: str, symbol: str = None) -> requests.Response:
|
|
441
455
|
"""
|
|
442
456
|
All transactions for a specific account. Maximum number of transactions in response is 3000. Maximum date range is 1 year.
|
|
443
457
|
:param accountHash: account hash number
|
|
@@ -488,15 +502,15 @@ class Client:
|
|
|
488
502
|
Market Data
|
|
489
503
|
"""
|
|
490
504
|
|
|
491
|
-
def quotes(self, symbols
|
|
505
|
+
def quotes(self, symbols : list[str] | str, fields: str = None, indicative: bool = False) -> requests.Response:
|
|
492
506
|
"""
|
|
493
507
|
Get quotes for a list of tickers
|
|
494
508
|
:param symbols: list of symbols strings (e.g. "AMD,INTC" or ["AMD", "INTC"])
|
|
495
509
|
:type symbols: [str] | str
|
|
496
|
-
:param fields:
|
|
497
|
-
:type fields:
|
|
510
|
+
:param fields: string of fields to get ("all", "quote", "fundamental")
|
|
511
|
+
:type fields: str | None
|
|
498
512
|
:param indicative: whether to get indicative quotes (True/False)
|
|
499
|
-
:type indicative: boolean
|
|
513
|
+
:type indicative: boolean | None
|
|
500
514
|
:return: list of quotes
|
|
501
515
|
:rtype: request.Response
|
|
502
516
|
"""
|
|
@@ -506,24 +520,24 @@ class Client:
|
|
|
506
520
|
{'symbols': self._format_list(symbols), 'fields': fields, 'indicative': indicative}),
|
|
507
521
|
timeout=self.timeout)
|
|
508
522
|
|
|
509
|
-
def quote(self, symbol_id: str, fields=None) -> requests.Response:
|
|
523
|
+
def quote(self, symbol_id: str, fields: str = None) -> requests.Response:
|
|
510
524
|
"""
|
|
511
525
|
Get quote for a single symbol
|
|
512
526
|
:param symbol_id: ticker symbol
|
|
513
527
|
:type symbol_id: str (e.g. "AAPL", "/ES", "USD/EUR")
|
|
514
|
-
:param fields:
|
|
515
|
-
:type fields:
|
|
528
|
+
:param fields: string of fields to get ("all", "quote", "fundamental")
|
|
529
|
+
:type fields: str | None
|
|
516
530
|
:return: quote for a single symbol
|
|
517
531
|
:rtype: request.Response
|
|
518
532
|
"""
|
|
519
|
-
return requests.get(f'{self._base_api_url}/marketdata/v1/{urllib.parse.
|
|
533
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/{urllib.parse.quote_plus(symbol_id)}/quotes',
|
|
520
534
|
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
521
535
|
params=self._params_parser({'fields': fields}),
|
|
522
536
|
timeout=self.timeout)
|
|
523
537
|
|
|
524
|
-
def option_chains(self, symbol: str, contractType=None, strikeCount=None, includeUnderlyingQuote=None, strategy=None,
|
|
525
|
-
interval=None, strike=None, range=None, fromDate=None, toDate=None, volatility=None, underlyingPrice=None,
|
|
526
|
-
interestRate=None, daysToExpiration=None, expMonth=None, optionType=None, entitlement=None) -> requests.Response:
|
|
538
|
+
def option_chains(self, symbol: str, contractType: str = None, strikeCount: any = None, includeUnderlyingQuote: bool = None, strategy: str = None,
|
|
539
|
+
interval: any = None, strike: any = None, range: str = None, fromDate: datetime.datetime | str = None, toDate: datetime.datetime | str = None, volatility: any = None, underlyingPrice: any = None,
|
|
540
|
+
interestRate: any = None, daysToExpiration: any = None, expMonth: str = None, optionType: str = None, entitlement: str = None) -> requests.Response:
|
|
527
541
|
"""
|
|
528
542
|
Get Option Chain including information on options contracts associated with each expiration for a ticker.
|
|
529
543
|
:param symbol: ticker symbol
|
|
@@ -587,8 +601,8 @@ class Client:
|
|
|
587
601
|
params=self._params_parser({'symbol': symbol}),
|
|
588
602
|
timeout=self.timeout)
|
|
589
603
|
|
|
590
|
-
def price_history(self, symbol: str, periodType=None, period=None, frequencyType=None, frequency=None, startDate=None,
|
|
591
|
-
endDate=None, needExtendedHoursData=None, needPreviousClose=None) -> requests.Response:
|
|
604
|
+
def price_history(self, symbol: str, periodType: str = None, period: any = None, frequencyType: str = None, frequency: any = None, startDate: datetime.datetime | str = None,
|
|
605
|
+
endDate: any = None, needExtendedHoursData: bool = None, needPreviousClose: bool = None) -> requests.Response:
|
|
592
606
|
"""
|
|
593
607
|
Get price history for a ticker
|
|
594
608
|
:param symbol: ticker symbol
|
|
@@ -622,7 +636,7 @@ class Client:
|
|
|
622
636
|
'needPreviousClose': needPreviousClose}),
|
|
623
637
|
timeout=self.timeout)
|
|
624
638
|
|
|
625
|
-
def movers(self, symbol: str, sort=None, frequency=None) -> requests.Response:
|
|
639
|
+
def movers(self, symbol: str, sort: str = None, frequency: any = None) -> requests.Response:
|
|
626
640
|
"""
|
|
627
641
|
Get movers in a specific index and direction
|
|
628
642
|
:param symbol: symbol ("$DJI"|"$COMPX"|"$SPX"|"NYSE"|"NASDAQ"|"OTCBB"|"INDEX_ALL"|"EQUITY_ALL"|"OPTION_ALL"|"OPTION_PUT"|"OPTION_CALL")
|
|
@@ -639,7 +653,7 @@ class Client:
|
|
|
639
653
|
params=self._params_parser({'sort': sort, 'frequency': frequency}),
|
|
640
654
|
timeout=self.timeout)
|
|
641
655
|
|
|
642
|
-
def market_hours(self, symbols, date=None) -> requests.Response:
|
|
656
|
+
def market_hours(self, symbols: list[str], date: datetime.datetime | str = None) -> requests.Response:
|
|
643
657
|
"""
|
|
644
658
|
Get Market Hours for dates in the future across different markets.
|
|
645
659
|
:param symbols: list of market symbols ("equity", "option", "bond", "future", "forex")
|
|
@@ -656,7 +670,7 @@ class Client:
|
|
|
656
670
|
'date': self._time_convert(date, 'YYYY-MM-DD')}),
|
|
657
671
|
timeout=self.timeout)
|
|
658
672
|
|
|
659
|
-
def market_hour(self, market_id: str, date=None) -> requests.Response:
|
|
673
|
+
def market_hour(self, market_id: str, date: datetime.datetime | str = None) -> requests.Response:
|
|
660
674
|
"""
|
|
661
675
|
Get Market Hours for dates in the future for a single market.
|
|
662
676
|
:param market_id: market id ("equity"|"option"|"bond"|"future"|"forex")
|
|
@@ -671,7 +685,7 @@ class Client:
|
|
|
671
685
|
params=self._params_parser({'date': self._time_convert(date, 'YYYY-MM-DD')}),
|
|
672
686
|
timeout=self.timeout)
|
|
673
687
|
|
|
674
|
-
def instruments(self, symbol: str, projection) -> requests.Response:
|
|
688
|
+
def instruments(self, symbol: str, projection: str) -> requests.Response:
|
|
675
689
|
"""
|
|
676
690
|
Get instruments for a list of symbols
|
|
677
691
|
:param symbol: symbol
|
|
@@ -7,11 +7,11 @@ Github: https://github.com/tylerebowers/Schwab-API-Python
|
|
|
7
7
|
import json
|
|
8
8
|
import atexit
|
|
9
9
|
import asyncio
|
|
10
|
+
import datetime
|
|
10
11
|
import threading
|
|
11
12
|
import websockets
|
|
12
13
|
from time import sleep
|
|
13
14
|
import websockets.exceptions
|
|
14
|
-
from datetime import datetime, time
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class Stream:
|
|
@@ -28,7 +28,6 @@ class Stream:
|
|
|
28
28
|
self.active = False # whether the stream is active
|
|
29
29
|
self._thread = None # the thread that runs the stream
|
|
30
30
|
self._client = client # so we can get streamer info
|
|
31
|
-
self.verbose = client.verbose # inherit the client's verbose setting
|
|
32
31
|
self.subscriptions = {} # a dictionary of subscriptions
|
|
33
32
|
|
|
34
33
|
# register atexit to stop the stream (if active)
|
|
@@ -38,7 +37,7 @@ class Stream:
|
|
|
38
37
|
atexit.register(stop_atexit)
|
|
39
38
|
|
|
40
39
|
|
|
41
|
-
async def _start_streamer(self, receiver_func=print,
|
|
40
|
+
async def _start_streamer(self, receiver_func=print, **kwargs):
|
|
42
41
|
"""
|
|
43
42
|
Start the streamer
|
|
44
43
|
:param receiver_func: function to call when data is received
|
|
@@ -49,15 +48,17 @@ class Stream:
|
|
|
49
48
|
if response.ok:
|
|
50
49
|
self._streamer_info = response.json().get('streamerInfo', None)[0]
|
|
51
50
|
else:
|
|
52
|
-
print("Could not get streamerInfo")
|
|
51
|
+
print("[Schwabdev] Could not get streamerInfo")
|
|
52
|
+
return
|
|
53
53
|
|
|
54
54
|
# start the stream
|
|
55
|
-
start_time = datetime.now()
|
|
55
|
+
start_time = datetime.datetime.now(datetime.timezone.utc)
|
|
56
56
|
while True:
|
|
57
57
|
try:
|
|
58
|
-
start_time = datetime.now()
|
|
59
|
-
if self.verbose: print("Connecting to streaming server...")
|
|
58
|
+
start_time = datetime.datetime.now(datetime.timezone.utc)
|
|
59
|
+
if self._client.verbose: print("[Schwabdev] Connecting to streaming server...")
|
|
60
60
|
async with websockets.connect(self._streamer_info.get('streamerSocketUrl'), ping_interval=None) as self._websocket:
|
|
61
|
+
if self._client.verbose: print("[Schwabdev] Connected to streaming server.")
|
|
61
62
|
# send login payload
|
|
62
63
|
login_payload = self.basic_request(service="ADMIN",
|
|
63
64
|
command="LOGIN",
|
|
@@ -65,7 +66,7 @@ class Stream:
|
|
|
65
66
|
"SchwabClientChannel": self._streamer_info.get("schwabClientChannel"),
|
|
66
67
|
"SchwabClientFunctionId": self._streamer_info.get("schwabClientFunctionId")})
|
|
67
68
|
await self._websocket.send(json.dumps(login_payload))
|
|
68
|
-
receiver_func(await self._websocket.recv(),
|
|
69
|
+
receiver_func(await self._websocket.recv(), **kwargs)
|
|
69
70
|
self.active = True
|
|
70
71
|
|
|
71
72
|
# send subscriptions
|
|
@@ -78,44 +79,45 @@ class Stream:
|
|
|
78
79
|
"fields": Stream._list_to_string(fields)}))
|
|
79
80
|
if reqs:
|
|
80
81
|
await self._websocket.send(json.dumps({"requests": reqs}))
|
|
81
|
-
receiver_func(await self._websocket.recv(),
|
|
82
|
+
receiver_func(await self._websocket.recv(), **kwargs)
|
|
82
83
|
|
|
83
84
|
# main listener loop
|
|
84
85
|
while True:
|
|
85
|
-
receiver_func(await self._websocket.recv(),
|
|
86
|
+
receiver_func(await self._websocket.recv(), **kwargs)
|
|
86
87
|
|
|
87
88
|
except Exception as e:
|
|
88
89
|
self.active = False
|
|
89
90
|
if e is websockets.exceptions.ConnectionClosedOK or str(e) == "received 1000 (OK); then sent 1000 (OK)": # catch logout request
|
|
90
|
-
if self.verbose: print("Stream
|
|
91
|
+
if self._client.verbose: print("[Schwabdev] Stream connection closed.")
|
|
91
92
|
break
|
|
92
93
|
elif e is websockets.exceptions.ConnectionClosedError or str(e) == "no close frame received or sent": # catch no subscriptions kick
|
|
93
|
-
|
|
94
|
+
print(f"[Schwabdev] Stream connection closed (likely no subscriptions): {e}")
|
|
94
95
|
break
|
|
95
|
-
elif (datetime.now() - start_time).seconds <= 90:
|
|
96
|
-
|
|
96
|
+
elif (datetime.datetime.now(datetime.timezone.utc) - start_time).seconds <= 90:
|
|
97
|
+
print(f"[Schwabdev] Stream has crashed within 90 seconds ({e}), likely no subscriptions, invalid login, or lost connection (not restarting).")
|
|
97
98
|
break
|
|
98
99
|
else: # stream has quit unexpectedly, try to reconnect
|
|
99
|
-
|
|
100
|
-
if self.verbose: print("Connection lost to server, reconnecting...")
|
|
100
|
+
print(f"[Schwabdev] Stream connection lost to server ({e}), reconnecting...")
|
|
101
101
|
|
|
102
|
-
def start(self, receiver=print,
|
|
102
|
+
def start(self, receiver=print, daemon: bool = True, **kwargs):
|
|
103
103
|
"""
|
|
104
104
|
Start the stream
|
|
105
105
|
:param receiver: function to call when data is received
|
|
106
106
|
:type receiver: function
|
|
107
|
+
:param daemon: whether to run the thread in the background (as a daemon)
|
|
108
|
+
:type daemon: bool
|
|
107
109
|
"""
|
|
108
110
|
if not self.active:
|
|
109
111
|
def _start_async():
|
|
110
|
-
asyncio.run(self._start_streamer(receiver,
|
|
112
|
+
asyncio.run(self._start_streamer(receiver, **kwargs))
|
|
111
113
|
|
|
112
|
-
self._thread = threading.Thread(target=_start_async, daemon=
|
|
114
|
+
self._thread = threading.Thread(target=_start_async, daemon=daemon)
|
|
113
115
|
self._thread.start()
|
|
114
116
|
# if the thread does not start in time then the main program may close before the streamer starts
|
|
115
117
|
else:
|
|
116
|
-
print("Stream already active.")
|
|
118
|
+
if self._client.verbose: print("[Schwabdev] Stream already active.")
|
|
117
119
|
|
|
118
|
-
def
|
|
120
|
+
def start_auto(self, receiver=print, after_hours=False, pre_hours=False, daemon: bool = True, **kwargs):
|
|
119
121
|
"""
|
|
120
122
|
Start the stream automatically at market open and close, will NOT erase subscriptions
|
|
121
123
|
:param receiver: function to call when data is received
|
|
@@ -125,32 +127,32 @@ class Stream:
|
|
|
125
127
|
:param pre_hours: include pre hours trading
|
|
126
128
|
:type pre_hours: bool
|
|
127
129
|
"""
|
|
128
|
-
start = time(
|
|
129
|
-
end = time(
|
|
130
|
+
start = datetime.time(13, 29, 0, tzinfo=datetime.timezone.utc) # market opens at 9:30 ET
|
|
131
|
+
end = datetime.time(20, 0, 0, tzinfo=datetime.timezone.utc) # market closes at 4:00 ET
|
|
130
132
|
if pre_hours:
|
|
131
|
-
start = time(
|
|
133
|
+
start = datetime.time(10, 59, 0, tzinfo=datetime.timezone.utc)
|
|
132
134
|
if after_hours:
|
|
133
|
-
end = time(
|
|
134
|
-
|
|
135
|
+
end = datetime.time.max.replace(tzinfo=datetime.timezone.utc) # 23:59:59:999999
|
|
135
136
|
def checker():
|
|
136
137
|
|
|
137
138
|
while True:
|
|
138
|
-
|
|
139
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
140
|
+
in_hours = (start <= now.time().replace(tzinfo=datetime.timezone.utc) <= end) and (0 <= now.weekday() <= 4)
|
|
139
141
|
if in_hours and not self.active:
|
|
140
142
|
if len(self.subscriptions) == 0:
|
|
141
|
-
if self.verbose: print("No subscriptions, starting stream anyways.")
|
|
142
|
-
self.start(receiver=receiver)
|
|
143
|
+
if self._client.verbose: print("[Schwabdev] No subscriptions, starting stream anyways.")
|
|
144
|
+
self.start(receiver=receiver, daemon=daemon, **kwargs)
|
|
143
145
|
elif not in_hours and self.active:
|
|
144
|
-
if self.verbose: print("Stopping Stream.")
|
|
146
|
+
if self._client.verbose: print("[Schwabdev] Stopping Stream.")
|
|
145
147
|
self.stop(clear_subscriptions=False)
|
|
146
|
-
sleep(
|
|
148
|
+
sleep(30)
|
|
147
149
|
|
|
148
|
-
threading.Thread(target=checker).start()
|
|
150
|
+
threading.Thread(target=checker, daemon=daemon).start()
|
|
149
151
|
|
|
150
|
-
if not start <= datetime.now().time() <= end:
|
|
151
|
-
print("Stream was started outside of active hours and will launch when in hours.")
|
|
152
|
+
if not start <= datetime.datetime.now(datetime.timezone.utc).time().replace(tzinfo=datetime.timezone.utc) <= end:
|
|
153
|
+
print("[Schwabdev] Stream was started outside of active hours and will launch when in hours.")
|
|
152
154
|
|
|
153
|
-
def _record_request(self, request):
|
|
155
|
+
def _record_request(self, request: dict):
|
|
154
156
|
"""
|
|
155
157
|
Record the request into self.subscriptions (for the event of crashes)
|
|
156
158
|
:param request: request
|
|
@@ -188,7 +190,7 @@ class Stream:
|
|
|
188
190
|
|
|
189
191
|
|
|
190
192
|
|
|
191
|
-
def send(self, requests):
|
|
193
|
+
def send(self, requests: list | dict):
|
|
192
194
|
"""
|
|
193
195
|
Send a request to the stream
|
|
194
196
|
:param requests: list of requests or a single request
|
|
@@ -211,10 +213,33 @@ class Stream:
|
|
|
211
213
|
to_send = json.dumps({"requests": requests})
|
|
212
214
|
asyncio.run(_send(to_send))
|
|
213
215
|
else:
|
|
214
|
-
if self.verbose: print("Stream is not active, request queued.")
|
|
216
|
+
if self._client.verbose: print("[Schwabdev] Stream is not active, request queued.")
|
|
215
217
|
|
|
216
218
|
|
|
217
|
-
def
|
|
219
|
+
async def send_async(self, requests: list | dict):
|
|
220
|
+
"""
|
|
221
|
+
Send an async (must be awaited) request to the stream (functionally equivalent to send)
|
|
222
|
+
:param requests: list of requests or a single request
|
|
223
|
+
:type requests: list | dict
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
# make sure requests is a list
|
|
227
|
+
if type(requests) is not list:
|
|
228
|
+
requests = [requests]
|
|
229
|
+
|
|
230
|
+
# add requests to list of subscriptions
|
|
231
|
+
for request in requests:
|
|
232
|
+
self._record_request(request)
|
|
233
|
+
|
|
234
|
+
# send the request if the stream is active, queue otherwise
|
|
235
|
+
if self.active:
|
|
236
|
+
to_send = json.dumps({"requests": requests})
|
|
237
|
+
await self._websocket.send(to_send)
|
|
238
|
+
else:
|
|
239
|
+
if self._client.verbose: print("[Schwabdev] Stream is not active, request queued.")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def stop(self, clear_subscriptions: bool = True):
|
|
218
243
|
"""
|
|
219
244
|
Stop the stream
|
|
220
245
|
:param clear_subscriptions: clear records
|
|
@@ -226,7 +251,7 @@ class Stream:
|
|
|
226
251
|
self.send(self.basic_request(service="ADMIN", command="LOGOUT"))
|
|
227
252
|
self.active = False
|
|
228
253
|
|
|
229
|
-
def basic_request(self, service, command, parameters=None):
|
|
254
|
+
def basic_request(self, service: str, command: str, parameters: dict = None):
|
|
230
255
|
"""
|
|
231
256
|
Create a basic request (all requests follow this format)
|
|
232
257
|
:param service: service to use
|
|
@@ -242,27 +267,26 @@ class Stream:
|
|
|
242
267
|
response = self._client.preferences()
|
|
243
268
|
if response.ok:
|
|
244
269
|
self._streamer_info = response.json().get('streamerInfo', None)[0]
|
|
270
|
+
else:
|
|
271
|
+
print("[Schwabdev] Could not use/get streamerInfo")
|
|
272
|
+
return {}
|
|
245
273
|
|
|
246
274
|
# remove None parameters
|
|
247
275
|
if parameters is not None:
|
|
248
276
|
for key in parameters.keys():
|
|
249
277
|
if parameters[key] is None: del parameters[key]
|
|
250
278
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return request
|
|
260
|
-
else:
|
|
261
|
-
print("basic_request(): Could not use/get streamerInfo")
|
|
262
|
-
return None
|
|
279
|
+
self._request_id += 1
|
|
280
|
+
request = {"service": service.upper(),
|
|
281
|
+
"command": command.upper(),
|
|
282
|
+
"requestid": self._request_id,
|
|
283
|
+
"SchwabClientCustomerId": self._streamer_info.get("schwabClientCustomerId"),
|
|
284
|
+
"SchwabClientCorrelId": self._streamer_info.get("schwabClientCorrelId")}
|
|
285
|
+
if parameters is not None and len(parameters) > 0: request["parameters"] = parameters
|
|
286
|
+
return request
|
|
263
287
|
|
|
264
288
|
@staticmethod
|
|
265
|
-
def _list_to_string(ls):
|
|
289
|
+
def _list_to_string(ls: list | str):
|
|
266
290
|
"""
|
|
267
291
|
Convert a list to a string (e.g. [1, "B", 3] -> "1,B,3"), or passthrough if already a string
|
|
268
292
|
:param ls: list to convert
|
|
@@ -273,7 +297,7 @@ class Stream:
|
|
|
273
297
|
if type(ls) is str: return ls
|
|
274
298
|
elif type(ls) is list: return ",".join(map(str, ls))
|
|
275
299
|
|
|
276
|
-
def level_one_equities(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
300
|
+
def level_one_equities(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
277
301
|
"""
|
|
278
302
|
Level one equities
|
|
279
303
|
:param keys: list of keys to use (e.g. ["AMD", "INTC"])
|
|
@@ -287,7 +311,7 @@ class Stream:
|
|
|
287
311
|
"""
|
|
288
312
|
return self.basic_request("LEVELONE_EQUITIES", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
289
313
|
|
|
290
|
-
def level_one_options(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
314
|
+
def level_one_options(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
291
315
|
"""
|
|
292
316
|
Level one options, key format: [Underlying Symbol (6 characters including spaces) | Expiration (6 characters) | Call/Put (1 character) | Strike Price (5+3=8 characters)]
|
|
293
317
|
:param keys: list of keys to use (e.g. ["GOOG 240809C00095000", "AAPL 240517P00190000"])
|
|
@@ -301,7 +325,7 @@ class Stream:
|
|
|
301
325
|
"""
|
|
302
326
|
return self.basic_request("LEVELONE_OPTIONS", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
303
327
|
|
|
304
|
-
def level_one_futures(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
328
|
+
def level_one_futures(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
305
329
|
"""
|
|
306
330
|
Level one futures, key format: '/' + 'root symbol' + 'month code' + 'year code'; month code is 1 character: (F: Jan, G: Feb, H: Mar, J: Apr, K: May, M: Jun, N: Jul, Q: Aug, U: Sep, V: Oct, X: Nov, Z: Dec), year code is 2 characters (i.e. 2024 = 24)
|
|
307
331
|
:param keys: list of keys to use (e.g. ["/ESF24", "/GCG24"])
|
|
@@ -315,7 +339,7 @@ class Stream:
|
|
|
315
339
|
"""
|
|
316
340
|
return self.basic_request("LEVELONE_FUTURES", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
317
341
|
|
|
318
|
-
def level_one_futures_options(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
342
|
+
def level_one_futures_options(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
319
343
|
"""
|
|
320
344
|
Level one futures options, key format: '.' + '/' + 'root symbol' + 'month code' + 'year code' + 'Call/Put code' + 'Strike Price'
|
|
321
345
|
:param keys: list of keys to use (e.g. ["./OZCZ23C565"])
|
|
@@ -329,7 +353,7 @@ class Stream:
|
|
|
329
353
|
"""
|
|
330
354
|
return self.basic_request("LEVELONE_FUTURES_OPTIONS", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
331
355
|
|
|
332
|
-
def level_one_forex(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
356
|
+
def level_one_forex(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
333
357
|
"""
|
|
334
358
|
Level one forex, key format: 'from currency' + '/' + 'to currency'
|
|
335
359
|
:param keys: list of keys to use (e.g. ["EUR/USD", "JPY/USD"])
|
|
@@ -343,7 +367,7 @@ class Stream:
|
|
|
343
367
|
"""
|
|
344
368
|
return self.basic_request("LEVELONE_FOREX", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
345
369
|
|
|
346
|
-
def nyse_book(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
370
|
+
def nyse_book(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
347
371
|
"""
|
|
348
372
|
NYSE book orders
|
|
349
373
|
:param keys: list of keys to use (e.g. ["NIO", "F"])
|
|
@@ -357,7 +381,7 @@ class Stream:
|
|
|
357
381
|
"""
|
|
358
382
|
return self.basic_request("NYSE_BOOK", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
359
383
|
|
|
360
|
-
def nasdaq_book(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
384
|
+
def nasdaq_book(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
361
385
|
"""
|
|
362
386
|
NASDAQ book orders
|
|
363
387
|
:param keys: list of keys to use (e.g. ["AMD", "CRWD"])
|
|
@@ -371,7 +395,7 @@ class Stream:
|
|
|
371
395
|
"""
|
|
372
396
|
return self.basic_request("NASDAQ_BOOK", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
373
397
|
|
|
374
|
-
def options_book(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
398
|
+
def options_book(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
375
399
|
"""
|
|
376
400
|
Options book orders
|
|
377
401
|
:param keys: list of keys to use (e.g. ["GOOG 240809C00095000", "AAPL 240517P00190000"])
|
|
@@ -385,7 +409,7 @@ class Stream:
|
|
|
385
409
|
"""
|
|
386
410
|
return self.basic_request("OPTIONS_BOOK", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
387
411
|
|
|
388
|
-
def chart_equity(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
412
|
+
def chart_equity(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
389
413
|
"""
|
|
390
414
|
Chart equity
|
|
391
415
|
:param keys: list of keys to use (e.g. ["GOOG", "AAPL"])
|
|
@@ -399,7 +423,7 @@ class Stream:
|
|
|
399
423
|
"""
|
|
400
424
|
return self.basic_request("CHART_EQUITY", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
401
425
|
|
|
402
|
-
def chart_futures(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
426
|
+
def chart_futures(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
403
427
|
"""
|
|
404
428
|
Chart futures, key format: '/' + 'root symbol' + 'month code' + 'year code'; month code is 1 character: (F: Jan, G: Feb, H: Mar, J: Apr, K: May, M: Jun, N: Jul, Q: Aug, U: Sep, V: Oct, X: Nov, Z: Dec), year code is 2 characters (i.e. 2024 = 24)
|
|
405
429
|
:param keys: list of keys to use (e.g. ["/ESF24", "/GCG24"])
|
|
@@ -413,7 +437,7 @@ class Stream:
|
|
|
413
437
|
"""
|
|
414
438
|
return self.basic_request("CHART_FUTURES", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
415
439
|
|
|
416
|
-
def screener_equity(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
440
|
+
def screener_equity(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
417
441
|
"""
|
|
418
442
|
Screener equity, key format: (PREFIX)_(SORTFIELD)_(FREQUENCY); Prefix: ($COMPX, $DJI, $SPX.X, INDEX_AL, NYSE, NASDAQ, OTCBB, EQUITY_ALL); Sortfield: (VOLUME, TRADES, PERCENT_CHANGE_UP, PERCENT_CHANGE_DOWN, AVERAGE_PERCENT_VOLUME), Frequency: (0 (all day), 1, 5, 10, 30 60)
|
|
419
443
|
:param keys: list of keys to use (e.g. ["$DJI_PERCENT_CHANGE_UP_60", "NASDAQ_VOLUME_30"])
|
|
@@ -427,7 +451,7 @@ class Stream:
|
|
|
427
451
|
"""
|
|
428
452
|
return self.basic_request("SCREENER_EQUITY", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
429
453
|
|
|
430
|
-
def screener_options(self, keys: str | list, fields: str | list, command="ADD") -> dict:
|
|
454
|
+
def screener_options(self, keys: str | list, fields: str | list, command: str = "ADD") -> dict:
|
|
431
455
|
"""
|
|
432
456
|
Screener option key format: (PREFIX)_(SORTFIELD)_(FREQUENCY); Prefix: (OPTION_PUT, OPTION_CALL, OPTION_ALL); Sortfield: (VOLUME, TRADES, PERCENT_CHANGE_UP, PERCENT_CHANGE_DOWN, AVERAGE_PERCENT_VOLUME), Frequency: (0 (all day), 1, 5, 10, 30 60)
|
|
433
457
|
:param keys: list of keys to use (e.g. ["OPTION_PUT_PERCENT_CHANGE_UP_60", "OPTION_CALL_TRADES_30"])
|
|
@@ -441,7 +465,7 @@ class Stream:
|
|
|
441
465
|
"""
|
|
442
466
|
return self.basic_request("SCREENER_OPTION", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
443
467
|
|
|
444
|
-
def account_activity(self, keys="Account Activity", fields="0,1,2,3", command="SUBS") -> dict:
|
|
468
|
+
def account_activity(self, keys="Account Activity", fields="0,1,2,3", command: str = "SUBS") -> dict:
|
|
445
469
|
"""
|
|
446
470
|
Account activity
|
|
447
471
|
:param keys: list of keys to use (e.g. ["Account Activity"])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: schwabdev
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: An easy and lightweight wrapper for using the Charles Schwab API.
|
|
5
5
|
Author: Tyler Bowers
|
|
6
6
|
Author-email: tylerebowers@gmail.com
|
|
@@ -28,20 +28,22 @@ This is an unofficial python program to access the Schwab api.
|
|
|
28
28
|
[Discord](https://discord.gg/m7SSjr9rs9), [PyPI](https://pypi.org/project/schwabdev/), [Youtube](https://youtube.com/playlist?list=PLs4JLWxBQIxpbvCj__DjAc0RRTlBz-TR8), [Github](https://github.com/tylerebowers/Schwab-API-Python).
|
|
29
29
|
|
|
30
30
|
## Installation
|
|
31
|
-
`pip install schwabdev
|
|
31
|
+
`pip install schwabdev`
|
|
32
32
|
*You may need to use `pip3` instead of `pip`*
|
|
33
33
|
|
|
34
34
|
## Quick setup
|
|
35
35
|
1. Setup your Schwab developer account [here](https://beta-developer.schwab.com/).
|
|
36
36
|
- Create a new Schwab individual developer app with callback url "https://127.0.0.1" (case sensitive)
|
|
37
|
+
- Add both API products to your app: "Accounts and Trading Production" and "Market Data Production".
|
|
37
38
|
- Wait until the status is "Ready for use", note that "Approved - Pending" will not work.
|
|
38
39
|
- Enable TOS (Thinkorswim) for your Schwab account, it is needed for orders and other api calls.
|
|
39
40
|
2. Install packages
|
|
40
|
-
- Install schwabdev and requirements `pip install schwabdev
|
|
41
|
+
- Install schwabdev and requirements `pip install schwabdev`
|
|
41
42
|
- *You may need to use `pip3` instead of `pip`*
|
|
42
43
|
3. Examples on how to use the client are in the `examples/` folder (add your keys in the .env file)
|
|
43
|
-
- The first time you run you will have to sign in to your Schwab account using the generated link in the terminal.
|
|
44
|
-
-
|
|
44
|
+
- The first time you run you will have to sign in to your Schwab account using the generated link in the terminal.
|
|
45
|
+
- After signing in, agree to the terms, and select account(s). Then you will have to copy the link in the address bar and paste it into the terminal.
|
|
46
|
+
- Questions? - join the [Discord group](https://discord.gg/m7SSjr9rs9) or consult the `/docs` folder.
|
|
45
47
|
```py
|
|
46
48
|
import schwabdev #import the package
|
|
47
49
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|