schwabdev 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schwabdev-1.0.0/PKG-INFO +60 -0
- schwabdev-1.0.0/README.md +40 -0
- schwabdev-1.0.0/schwabdev/__init__.py +1 -0
- schwabdev-1.0.0/schwabdev/api.py +393 -0
- schwabdev-1.0.0/schwabdev/stream.py +304 -0
- schwabdev-1.0.0/schwabdev/terminal.py +89 -0
- schwabdev-1.0.0/schwabdev.egg-info/PKG-INFO +60 -0
- schwabdev-1.0.0/schwabdev.egg-info/SOURCES.txt +11 -0
- schwabdev-1.0.0/schwabdev.egg-info/dependency_links.txt +1 -0
- schwabdev-1.0.0/schwabdev.egg-info/requires.txt +2 -0
- schwabdev-1.0.0/schwabdev.egg-info/top_level.txt +1 -0
- schwabdev-1.0.0/setup.cfg +4 -0
- schwabdev-1.0.0/setup.py +37 -0
schwabdev-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: schwabdev
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Schwab API Python Client (unofficial)
|
|
5
|
+
Author: Tyler Bowers
|
|
6
|
+
Author-email: tylerebowers@gmail.com
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Source, https://github.com/tylerebowers/Schwab-API-Python
|
|
9
|
+
Project-URL: Youtube, https://www.youtube.com/playlist?list=PLs4JLWxBQIxpbvCj__DjAc0RRTlBz-TR8
|
|
10
|
+
Project-URL: PyPI, https://pypi.org/project/schwabdev/
|
|
11
|
+
Keywords: python,schwab,api,client,finance,trading,stocks,equities,options,forex,futures
|
|
12
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Schwab-API-Python
|
|
22
|
+
This is an unofficial python program to access the Schwab api.
|
|
23
|
+
You will need a Schwab developer account [here](https://beta-developer.schwab.com/).
|
|
24
|
+
Join the [Discord group](https://discord.gg/m7SSjr9rs9).
|
|
25
|
+
Also found on [PyPI](https://pypi.org/project/schwabdev/), install via `pip3 install schwabdev`
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Quick setup
|
|
29
|
+
1. Create a new Schwab individual developer app with callback url "https://127.0.0.1" (case sensitive) and wait until the status is "Ready for use", note that "Approved - Pending" will not work.
|
|
30
|
+
2. Enable TOS (Thinkorswim) for your Schwab account, it is needed for orders and other api calls.
|
|
31
|
+
3. Python version 3.11 or higher is required.
|
|
32
|
+
4. `pip3 install schwabdev requests websockets tk` (tkinter/tk may need to be installed differently)
|
|
33
|
+
5. Import the package `import schwabdev`
|
|
34
|
+
6. Create a client `client = schwabdev.Client('Your app key', 'Your app secret')`
|
|
35
|
+
7. Examples on how to use the client are in `tests/api_demo.py`
|
|
36
|
+
|
|
37
|
+
## What can this program do?
|
|
38
|
+
- Authenticate and access the api
|
|
39
|
+
- Functions for all api functions (examples in `tests/api_demo.py`)
|
|
40
|
+
- Auto "access token" updates (`client.update_tokens_auto()`)
|
|
41
|
+
- Stream real-time data with customizable response handler (examples in `tests/stream_demo.py`)
|
|
42
|
+
### TBD
|
|
43
|
+
- Automatic refresh token updates. (Waiting for Schwab implementation)
|
|
44
|
+
|
|
45
|
+
## Notes
|
|
46
|
+
|
|
47
|
+
The schwabdev folder contains code for main operations:
|
|
48
|
+
- `api.py` contains functions relating to api calls, requests, and automatic token checker threads.
|
|
49
|
+
- `stream.py` contains functions for streaming data from websockets.
|
|
50
|
+
- `terminal.py` contains a program for making additional terminal windows and printing to the terminal with color.
|
|
51
|
+
|
|
52
|
+
## License (MIT)
|
|
53
|
+
|
|
54
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
55
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
56
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
57
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
58
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
59
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
60
|
+
SOFTWARE.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Schwab-API-Python
|
|
2
|
+
This is an unofficial python program to access the Schwab api.
|
|
3
|
+
You will need a Schwab developer account [here](https://beta-developer.schwab.com/).
|
|
4
|
+
Join the [Discord group](https://discord.gg/m7SSjr9rs9).
|
|
5
|
+
Also found on [PyPI](https://pypi.org/project/schwabdev/), install via `pip3 install schwabdev`
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Quick setup
|
|
9
|
+
1. Create a new Schwab individual developer app with callback url "https://127.0.0.1" (case sensitive) and wait until the status is "Ready for use", note that "Approved - Pending" will not work.
|
|
10
|
+
2. Enable TOS (Thinkorswim) for your Schwab account, it is needed for orders and other api calls.
|
|
11
|
+
3. Python version 3.11 or higher is required.
|
|
12
|
+
4. `pip3 install schwabdev requests websockets tk` (tkinter/tk may need to be installed differently)
|
|
13
|
+
5. Import the package `import schwabdev`
|
|
14
|
+
6. Create a client `client = schwabdev.Client('Your app key', 'Your app secret')`
|
|
15
|
+
7. Examples on how to use the client are in `tests/api_demo.py`
|
|
16
|
+
|
|
17
|
+
## What can this program do?
|
|
18
|
+
- Authenticate and access the api
|
|
19
|
+
- Functions for all api functions (examples in `tests/api_demo.py`)
|
|
20
|
+
- Auto "access token" updates (`client.update_tokens_auto()`)
|
|
21
|
+
- Stream real-time data with customizable response handler (examples in `tests/stream_demo.py`)
|
|
22
|
+
### TBD
|
|
23
|
+
- Automatic refresh token updates. (Waiting for Schwab implementation)
|
|
24
|
+
|
|
25
|
+
## Notes
|
|
26
|
+
|
|
27
|
+
The schwabdev folder contains code for main operations:
|
|
28
|
+
- `api.py` contains functions relating to api calls, requests, and automatic token checker threads.
|
|
29
|
+
- `stream.py` contains functions for streaming data from websockets.
|
|
30
|
+
- `terminal.py` contains a program for making additional terminal windows and printing to the terminal with color.
|
|
31
|
+
|
|
32
|
+
## License (MIT)
|
|
33
|
+
|
|
34
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
35
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
36
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
37
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
38
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
39
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
40
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .api import Client
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import base64
|
|
3
|
+
import requests
|
|
4
|
+
import threading
|
|
5
|
+
import urllib.parse
|
|
6
|
+
from .stream import Stream
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from schwabdev import terminal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Client:
|
|
12
|
+
|
|
13
|
+
def __init__(self, app_key, app_secret, callback_url="https://127.0.0.1", tokens_file="tokens.json"):
|
|
14
|
+
if app_key is None or app_secret is None or callback_url is None or tokens_file is None:
|
|
15
|
+
raise Exception("app_key, app_secret, callback_url, and tokens_file cannot be None.")
|
|
16
|
+
elif len(app_key) != 32 or len(app_secret) != 16:
|
|
17
|
+
raise Exception("App key or app secret invalid length.")
|
|
18
|
+
|
|
19
|
+
self._app_key = app_key
|
|
20
|
+
self._app_secret = app_secret
|
|
21
|
+
self._callback_url = callback_url
|
|
22
|
+
self.access_token = None
|
|
23
|
+
self.refresh_token = None
|
|
24
|
+
self.id_token = None
|
|
25
|
+
self._access_token_issued = None # datetime of access token issue
|
|
26
|
+
self._refresh_token_issued = None # datetime of refresh token issue
|
|
27
|
+
self._access_token_timeout = 1800 # in seconds (from schwab)
|
|
28
|
+
self._refresh_token_timeout = 7 # in days (from schwab)
|
|
29
|
+
self._tokens_file = tokens_file # path to tokens file
|
|
30
|
+
self.stream = Stream(self)
|
|
31
|
+
|
|
32
|
+
# Try to load tokens from the tokens file
|
|
33
|
+
at_issued, rt_issued, token_dictionary = self._read_tokens_file()
|
|
34
|
+
if None not in [at_issued, rt_issued, token_dictionary]:
|
|
35
|
+
# show user when tokens were last updated and when they will expire
|
|
36
|
+
self.access_token = token_dictionary.get("access_token")
|
|
37
|
+
self.refresh_token = token_dictionary.get("refresh_token")
|
|
38
|
+
self.id_token = token_dictionary.get("id_token")
|
|
39
|
+
self._access_token_issued = at_issued
|
|
40
|
+
self._refresh_token_issued = rt_issued
|
|
41
|
+
terminal.color_print.info(self._access_token_issued.strftime(
|
|
42
|
+
"Access token last updated: %Y-%m-%d %H:%M:%S") + f" (expires in {self._access_token_timeout - (datetime.now() - self._access_token_issued).seconds} seconds)")
|
|
43
|
+
terminal.color_print.info(self._refresh_token_issued.strftime(
|
|
44
|
+
"Refresh token last updated: %Y-%m-%d %H:%M:%S") + f" (expires in {self._refresh_token_timeout - (datetime.now() - self._refresh_token_issued).days} days)")
|
|
45
|
+
# check if tokens need to be updated and update if needed
|
|
46
|
+
self.update_tokens()
|
|
47
|
+
else:
|
|
48
|
+
# The tokens file doesn't exist, so create it.
|
|
49
|
+
terminal.color_print.warning(f"Token file does not exist or invalid formatting, creating \"{str(tokens_file)}\"")
|
|
50
|
+
open(self._tokens_file, 'w').close()
|
|
51
|
+
# Tokens must be updated.
|
|
52
|
+
self._update_refresh_token()
|
|
53
|
+
|
|
54
|
+
# get account numbers & hashes, this doubles as a checker to make sure that the appKey and appSecret are valid and that the app is ready for use
|
|
55
|
+
resp = self.account_linked()
|
|
56
|
+
if resp.ok:
|
|
57
|
+
d = resp.json()
|
|
58
|
+
terminal.color_print.info(f"Linked Accounts: {d}")
|
|
59
|
+
else: # app might not be "Ready For Use"
|
|
60
|
+
terminal.color_print.error("Could not get linked accounts.")
|
|
61
|
+
terminal.color_print.error("Please make sure that your app status is \"Ready For Use\" and that the app key and app secret are valid.")
|
|
62
|
+
terminal.color_print.error(resp.json())
|
|
63
|
+
resp.close()
|
|
64
|
+
|
|
65
|
+
terminal.color_print.info("Initialization Complete")
|
|
66
|
+
|
|
67
|
+
def update_tokens(self):
|
|
68
|
+
if (datetime.now() - self._refresh_token_issued).days >= (
|
|
69
|
+
self._refresh_token_timeout - 1): # check if we need to update refresh (and access) token
|
|
70
|
+
for i in range(5): terminal.color_print.user("The refresh token has expired, please update!")
|
|
71
|
+
self._update_refresh_token()
|
|
72
|
+
elif ((datetime.now() - self._access_token_issued).days >= 1) or (
|
|
73
|
+
(datetime.now() - self._access_token_issued).seconds > (
|
|
74
|
+
self._access_token_timeout - 60)): # check if we need to update access token
|
|
75
|
+
terminal.color_print.info("The access token has expired, updating automatically.")
|
|
76
|
+
self._update_access_token()
|
|
77
|
+
# else: terminal.color_print.info("Token check passed")
|
|
78
|
+
|
|
79
|
+
def update_tokens_auto(self):
|
|
80
|
+
def checker():
|
|
81
|
+
import time
|
|
82
|
+
while True:
|
|
83
|
+
self.update_tokens()
|
|
84
|
+
time.sleep(60)
|
|
85
|
+
|
|
86
|
+
threading.Thread(target=checker, daemon=True).start()
|
|
87
|
+
|
|
88
|
+
# "refresh" the access token using the refresh token
|
|
89
|
+
def _update_access_token(self):
|
|
90
|
+
# get the token dictionary (we will need to rewrite the file)
|
|
91
|
+
access_token_time_old, refresh_token_issued, token_dictionary_old = self._read_tokens_file()
|
|
92
|
+
# get new tokens
|
|
93
|
+
response = self._post_oauth_token('refresh_token', token_dictionary_old.get("refresh_token"))
|
|
94
|
+
if response.ok:
|
|
95
|
+
# get and update to the new access token
|
|
96
|
+
self._access_token_issued = datetime.now()
|
|
97
|
+
self._refresh_token_issued = refresh_token_issued
|
|
98
|
+
new_td = response.json()
|
|
99
|
+
self.access_token = new_td.get("access_token")
|
|
100
|
+
self.refresh_token = new_td.get("refresh_token")
|
|
101
|
+
self.id_token = new_td.get("id_token")
|
|
102
|
+
self._write_tokens_file(self._access_token_issued, refresh_token_issued, new_td)
|
|
103
|
+
# show user that we have updated the access token
|
|
104
|
+
terminal.color_print.info(f"Access token updated: {self._access_token_issued}")
|
|
105
|
+
else:
|
|
106
|
+
terminal.color_print.error("Could not get new access token.")
|
|
107
|
+
|
|
108
|
+
# get new access and refresh tokens using authorization code.
|
|
109
|
+
def _update_refresh_token(self):
|
|
110
|
+
import webbrowser
|
|
111
|
+
# get authorization code (requires user to authorize)
|
|
112
|
+
terminal.color_print.user("Please authorize this program to access your schwab account.")
|
|
113
|
+
auth_url = f'https://api.schwabapi.com/v1/oauth/authorize?client_id={self._app_key}&redirect_uri={self._callback_url}'
|
|
114
|
+
terminal.color_print.user(f"Click to authenticate: {auth_url}")
|
|
115
|
+
terminal.color_print.user("Opening browser..")
|
|
116
|
+
webbrowser.open(auth_url)
|
|
117
|
+
response_url = terminal.color_print.input(
|
|
118
|
+
"After authorizing, wait for it to load (<1min) and paste the WHOLE url here: ")
|
|
119
|
+
code = f"{response_url[response_url.index('code=') + 5:response_url.index('%40')]}@" # session = responseURL[responseURL.index("session=")+8:]
|
|
120
|
+
# get new access and refresh tokens
|
|
121
|
+
response = self._post_oauth_token('authorization_code', code)
|
|
122
|
+
if response.ok:
|
|
123
|
+
# update token file and variables
|
|
124
|
+
self._access_token_issued = self._refresh_token_issued = datetime.now()
|
|
125
|
+
new_td = response.json()
|
|
126
|
+
self.access_token = new_td.get("access_token")
|
|
127
|
+
self.refresh_token = new_td.get("refresh_token")
|
|
128
|
+
self.id_token = new_td.get("id_token")
|
|
129
|
+
self._write_tokens_file(self._access_token_issued, self._refresh_token_issued, new_td)
|
|
130
|
+
terminal.color_print.info("Refresh and Access tokens updated")
|
|
131
|
+
else:
|
|
132
|
+
terminal.color_print.error("Could not get new refresh and access tokens.")
|
|
133
|
+
terminal.color_print.error(
|
|
134
|
+
"Please make sure that your app status is \"Ready For Use\" and that the app key and app secret are valid.")
|
|
135
|
+
|
|
136
|
+
def _post_oauth_token(self, grant_type, code):
|
|
137
|
+
headers = {
|
|
138
|
+
'Authorization': f'Basic {base64.b64encode(bytes(f"{self._app_key}:{self._app_secret}", "utf-8")).decode("utf-8")}',
|
|
139
|
+
'Content-Type': 'application/x-www-form-urlencoded'}
|
|
140
|
+
if grant_type == 'authorization_code': # gets access and refresh tokens using authorization code
|
|
141
|
+
data = {'grant_type': 'authorization_code', 'code': code,
|
|
142
|
+
'redirect_uri': self._callback_url}
|
|
143
|
+
elif grant_type == 'refresh_token': # refreshes the access token
|
|
144
|
+
data = {'grant_type': 'refresh_token', 'refresh_token': code}
|
|
145
|
+
else:
|
|
146
|
+
terminal.color_print.error("Invalid grant type")
|
|
147
|
+
return None
|
|
148
|
+
return requests.post('https://api.schwabapi.com/v1/oauth/token', headers=headers, data=data)
|
|
149
|
+
|
|
150
|
+
def _write_tokens_file(self, atIssued, rtIssued, tokenDictionary):
|
|
151
|
+
# update tokens file
|
|
152
|
+
try:
|
|
153
|
+
with open(self._tokens_file, 'w') as f:
|
|
154
|
+
toWrite = {"access_token_issued": atIssued.isoformat(), "refresh_token_issued": rtIssued.isoformat(),
|
|
155
|
+
"token_dictionary": tokenDictionary}
|
|
156
|
+
json.dump(toWrite, f, ensure_ascii=False, indent=4)
|
|
157
|
+
f.flush()
|
|
158
|
+
except Exception as e:
|
|
159
|
+
terminal.color_print.error(e)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _read_tokens_file(self):
|
|
163
|
+
try:
|
|
164
|
+
with open(self._tokens_file, 'r') as f:
|
|
165
|
+
d = json.load(f)
|
|
166
|
+
return datetime.fromisoformat(d.get("access_token_issued")), datetime.fromisoformat(d.get("refresh_token_issued")), d.get("token_dictionary")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
terminal.color_print.error(e)
|
|
169
|
+
return None, None, None
|
|
170
|
+
|
|
171
|
+
def _params_parser(self, params):
|
|
172
|
+
for key in list(params.keys()):
|
|
173
|
+
if params[key] is None: del params[key]
|
|
174
|
+
return params
|
|
175
|
+
|
|
176
|
+
def _time_convert(self, dt=None, form="8601"):
|
|
177
|
+
if dt is None:
|
|
178
|
+
return None
|
|
179
|
+
elif dt is str:
|
|
180
|
+
return dt
|
|
181
|
+
elif form == "8601": # assume datetime object from here on
|
|
182
|
+
return f'{dt.isoformat()[:-3]}Z'
|
|
183
|
+
elif form == "epoch":
|
|
184
|
+
return int(dt.timestamp())
|
|
185
|
+
elif form == "epoch_ms":
|
|
186
|
+
return int(dt.timestamp() * 1000)
|
|
187
|
+
elif form == "YYYY-MM-DD":
|
|
188
|
+
return dt.strftime("%Y-%M-%d")
|
|
189
|
+
else:
|
|
190
|
+
return dt
|
|
191
|
+
|
|
192
|
+
def _format_list(self, l):
|
|
193
|
+
if l is None:
|
|
194
|
+
return None
|
|
195
|
+
elif type(l) is list:
|
|
196
|
+
return ",".join(l)
|
|
197
|
+
else:
|
|
198
|
+
return l
|
|
199
|
+
|
|
200
|
+
_base_api_url = "https://api.schwabapi.com"
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
Accounts and Trading Production
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
# Get account numbers and account hashes for linked accounts
|
|
207
|
+
def account_linked(self):
|
|
208
|
+
return requests.get(f'{self._base_api_url}/trader/v1/accounts/accountNumbers',
|
|
209
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
210
|
+
timeout=2)
|
|
211
|
+
|
|
212
|
+
# Get account details for all linked accounts, details such as balance, positions, buying power, etc.
|
|
213
|
+
def account_details_all(self, fields=None):
|
|
214
|
+
return requests.get(f'{self._base_api_url}/trader/v1/accounts/',
|
|
215
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
216
|
+
params=self._params_parser({'fields': fields}),
|
|
217
|
+
timeout=2)
|
|
218
|
+
|
|
219
|
+
# Get account details for one linked account, uses default account.
|
|
220
|
+
def account_details(self, accountHash, fields=None):
|
|
221
|
+
return requests.get(f'{self._base_api_url}/trader/v1/accounts/{accountHash}',
|
|
222
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
223
|
+
params=self._params_parser({'fields': fields}),
|
|
224
|
+
timeout=2)
|
|
225
|
+
|
|
226
|
+
# Get all orders for one linked account, uses default account.
|
|
227
|
+
def account_orders(self, accountHash, maxResults, fromEnteredTime, toEnteredTime, status=None):
|
|
228
|
+
return requests.get(f'{self._base_api_url}/trader/v1/accounts/{accountHash}/orders',
|
|
229
|
+
headers={"Accept": "application/json", 'Authorization': f'Bearer {self.access_token}'},
|
|
230
|
+
params=self._params_parser(
|
|
231
|
+
{'maxResults': maxResults, 'fromEnteredTime': self._time_convert(fromEnteredTime, "8601"),
|
|
232
|
+
'toEnteredTime': self._time_convert(toEnteredTime, "8601"), 'status': status}),
|
|
233
|
+
timeout=2)
|
|
234
|
+
|
|
235
|
+
# place an order for one linked account (uses default account)
|
|
236
|
+
def order_place(self, accountHash, order):
|
|
237
|
+
return requests.post(f'{self._base_api_url}/trader/v1/accounts/{accountHash}/orders',
|
|
238
|
+
headers={"Accept": "application/json", 'Authorization': f'Bearer {self.access_token}',
|
|
239
|
+
"Content-Type": "application/json"},
|
|
240
|
+
json=order,
|
|
241
|
+
timeout=2)
|
|
242
|
+
|
|
243
|
+
# get order details using order id (uses default account)
|
|
244
|
+
def order_details(self, accountHash, orderId):
|
|
245
|
+
return requests.get(f'{self._base_api_url}/trader/v1/accounts/{accountHash}/orders/{orderId}',
|
|
246
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
247
|
+
timeout=2)
|
|
248
|
+
|
|
249
|
+
# cancel order using order id (uses default account)
|
|
250
|
+
def order_cancel(self, accountHash, orderId):
|
|
251
|
+
return requests.delete(f'{self._base_api_url}/trader/v1/accounts/{accountHash}/orders/{orderId}',
|
|
252
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
253
|
+
timeout=2)
|
|
254
|
+
|
|
255
|
+
# replace order using order id (uses default account)
|
|
256
|
+
def order_replace(self, accountHash, orderId, order):
|
|
257
|
+
return requests.put(f'{self._base_api_url}/trader/v1/accounts/{accountHash}/orders/{orderId}',
|
|
258
|
+
headers={"Accept": "application/json", 'Authorization': f'Bearer {self.access_token}',
|
|
259
|
+
"Content-Type": "application/json"},
|
|
260
|
+
json=order,
|
|
261
|
+
timeout=2)
|
|
262
|
+
|
|
263
|
+
# get all orders across all linked accounts
|
|
264
|
+
def account_orders_all(self, maxResults, fromEnteredTime, toEnteredTime, status=None):
|
|
265
|
+
return requests.get(f'{self._base_api_url}/trader/v1/orders',
|
|
266
|
+
headers={"Accept": "application/json", 'Authorization': f'Bearer {self.access_token}'},
|
|
267
|
+
params=self._params_parser(
|
|
268
|
+
{'maxResults': maxResults, 'fromEnteredTime': self._time_convert(fromEnteredTime, "8601"),
|
|
269
|
+
'toEnteredTime': self._time_convert(toEnteredTime, "8601"), 'status': status}),
|
|
270
|
+
timeout=2)
|
|
271
|
+
|
|
272
|
+
""" #COMING SOON (waiting on Schwab)
|
|
273
|
+
# /accounts/{accountHash}/previewOrder
|
|
274
|
+
def order_preview(accountHash, orderObject):
|
|
275
|
+
|
|
276
|
+
return requests.post(f'{self._base_api_url}/trader/v1/accounts/{accountHash}/previewOrder',
|
|
277
|
+
headers={'Authorization': f'Bearer {self.access_token}',
|
|
278
|
+
"Content-Type": "application.json"}, data=orderObject)
|
|
279
|
+
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
# get all transactions (has maximums) for one linked account (uses default account)
|
|
283
|
+
def transactions(self, accountHash, startDate, endDate, types, symbol=None):
|
|
284
|
+
return requests.get(f'{self._base_api_url}/trader/v1/accounts/{accountHash}/transactions',
|
|
285
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
286
|
+
params=self._params_parser(
|
|
287
|
+
{'accountNumber': accountHash, 'startDate': self._time_convert(startDate, "8601"),
|
|
288
|
+
'endDate': self._time_convert(endDate, "8601"), 'symbol': symbol, 'types': types}),
|
|
289
|
+
timeout=2)
|
|
290
|
+
|
|
291
|
+
# get transaction details using transaction id (uses default account)
|
|
292
|
+
def transaction_details(self, accountHash, transactionId):
|
|
293
|
+
return requests.get(f'{self._base_api_url}/trader/v1/accounts/{accountHash}/transactions/{transactionId}',
|
|
294
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
295
|
+
params={'accountNumber': accountHash, 'transactionId': transactionId},
|
|
296
|
+
timeout=2)
|
|
297
|
+
|
|
298
|
+
# get user preferences, includes streaming info
|
|
299
|
+
def preferences(self):
|
|
300
|
+
return requests.get(f'{self._base_api_url}/trader/v1/userPreference',
|
|
301
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
302
|
+
timeout=2)
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
Market Data
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
# get quotes for a list of tickers
|
|
309
|
+
def quotes(self, symbols=None, fields=None, indicative=False):
|
|
310
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/quotes',
|
|
311
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
312
|
+
params=self._params_parser(
|
|
313
|
+
{'symbols': self._format_list(symbols), 'fields': fields, 'indicative': indicative}),
|
|
314
|
+
timeout=2)
|
|
315
|
+
|
|
316
|
+
# get a single quote for a ticker
|
|
317
|
+
def quote(self, symbol_id, fields=None):
|
|
318
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/{urllib.parse.quote(symbol_id)}/quotes',
|
|
319
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
320
|
+
params=self._params_parser({'fields': fields}),
|
|
321
|
+
timeout=2)
|
|
322
|
+
|
|
323
|
+
# get option chains for a ticker
|
|
324
|
+
def option_chains(self, symbol, contractType=None, strikeCount=None, includeUnderlyingQuotes=None, strategy=None,
|
|
325
|
+
interval=None,
|
|
326
|
+
strike=None, range=None, fromDate=None, toDate=None, volatility=None, underlyingPrice=None,
|
|
327
|
+
interestRate=None, daysToExpiration=None, expMonth=None, optionType=None, entitlement=None):
|
|
328
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/chains',
|
|
329
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
330
|
+
params=self._params_parser(
|
|
331
|
+
{'symbol': symbol, 'contractType': contractType, 'strikeCount': strikeCount,
|
|
332
|
+
'includeUnderlyingQuotes': includeUnderlyingQuotes, 'strategy': strategy,
|
|
333
|
+
'interval': interval, 'strike': strike, 'range': range, 'fromDate': fromDate,
|
|
334
|
+
'toDate': toDate, 'volatility': volatility, 'underlyingPrice': underlyingPrice,
|
|
335
|
+
'interestRate': interestRate, 'daysToExpiration': daysToExpiration,
|
|
336
|
+
'expMonth': expMonth, 'optionType': optionType, 'entitlement': entitlement}),
|
|
337
|
+
timeout=2)
|
|
338
|
+
|
|
339
|
+
# get an option expiration chain for a ticker
|
|
340
|
+
def option_expiration_chain(self, symbol):
|
|
341
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/expirationchain',
|
|
342
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
343
|
+
params=self._params_parser({'symbol': symbol}),
|
|
344
|
+
timeout=2)
|
|
345
|
+
|
|
346
|
+
# get price history for a ticker
|
|
347
|
+
def price_history(self, symbol, periodType=None, period=None, frequencyType=None, frequency=None, startDate=None,
|
|
348
|
+
endDate=None, needExtendedHoursData=None, needPreviousClose=None):
|
|
349
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/pricehistory',
|
|
350
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
351
|
+
params=self._params_parser({'symbol': symbol, 'periodType': periodType, 'period': period,
|
|
352
|
+
'frequencyType': frequencyType, 'frequency': frequency,
|
|
353
|
+
'startDate': self._time_convert(startDate, 'epoch_ms'),
|
|
354
|
+
'endDate': self._time_convert(endDate, 'epoch_ms'),
|
|
355
|
+
'needExtendedHoursData': needExtendedHoursData,
|
|
356
|
+
'needPreviousClose': needPreviousClose}),
|
|
357
|
+
timeout=2)
|
|
358
|
+
|
|
359
|
+
# get movers in a specific index and direction
|
|
360
|
+
def movers(self, symbol, sort=None, frequency=None):
|
|
361
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/movers/{symbol}',
|
|
362
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
363
|
+
params=self._params_parser({'sort': sort, 'frequency': frequency}),
|
|
364
|
+
timeout=2)
|
|
365
|
+
|
|
366
|
+
# get market hours for a list of markets
|
|
367
|
+
def market_hours(self, symbols, date=None):
|
|
368
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/markets',
|
|
369
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
370
|
+
params=self._params_parser(
|
|
371
|
+
{'markets': symbols, #self._format_list(symbols),
|
|
372
|
+
'date': self._time_convert(date, 'YYYY-MM-DD')}),
|
|
373
|
+
timeout=2)
|
|
374
|
+
|
|
375
|
+
# get market hours for a single market
|
|
376
|
+
def market_hour(self, market_id, date=None):
|
|
377
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/markets/{market_id}',
|
|
378
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
379
|
+
params=self._params_parser({'date': self._time_convert(date, 'YYYY-MM-DD')}),
|
|
380
|
+
timeout=2)
|
|
381
|
+
|
|
382
|
+
# get instruments for a list of symbols
|
|
383
|
+
def instruments(self, symbol, projection):
|
|
384
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/instruments',
|
|
385
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
386
|
+
params={'symbol': symbol, 'projection': projection},
|
|
387
|
+
timeout=2)
|
|
388
|
+
|
|
389
|
+
# get instruments for a single cusip
|
|
390
|
+
def instrument_cusip(self, cusip_id):
|
|
391
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/instruments/{cusip_id}',
|
|
392
|
+
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
393
|
+
timeout=2)
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file contains functions to stream data
|
|
3
|
+
Coded by Tyler Bowers
|
|
4
|
+
Github: https://github.com/tylerebowers/Schwab-API-Python
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import asyncio
|
|
9
|
+
import threading
|
|
10
|
+
import websockets
|
|
11
|
+
import websockets.exceptions
|
|
12
|
+
from time import sleep
|
|
13
|
+
from datetime import datetime, time
|
|
14
|
+
from schwabdev import terminal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Stream:
|
|
18
|
+
|
|
19
|
+
def __init__(self, client):
|
|
20
|
+
self._websocket = None
|
|
21
|
+
self._streamer_info = None
|
|
22
|
+
self._start_timestamp = None
|
|
23
|
+
self._terminal = None
|
|
24
|
+
self._request_id = 0 # a counter for the request id
|
|
25
|
+
self._queue = [] # a queue of requests to be sent
|
|
26
|
+
self.active = False
|
|
27
|
+
self.client = client # so we can get streamer info
|
|
28
|
+
|
|
29
|
+
async def _start_streamer(self, receiver_func="default"):
|
|
30
|
+
# get streamer info
|
|
31
|
+
response = self.client.preferences()
|
|
32
|
+
if response.ok:
|
|
33
|
+
self._streamer_info = response.json().get('streamerInfo', None)[0]
|
|
34
|
+
else:
|
|
35
|
+
terminal.color_print.error("Could not get streamerInfo")
|
|
36
|
+
|
|
37
|
+
# specify receiver (what do we do with received data)
|
|
38
|
+
if receiver_func == "default":
|
|
39
|
+
if self._terminal is None:
|
|
40
|
+
self._terminal = terminal.multiTerminal(title="Stream output")
|
|
41
|
+
|
|
42
|
+
def default_receiver(data):
|
|
43
|
+
self._terminal.print(data)
|
|
44
|
+
receiver_func = default_receiver
|
|
45
|
+
|
|
46
|
+
# start the stream
|
|
47
|
+
while True:
|
|
48
|
+
try:
|
|
49
|
+
self._start_timestamp = datetime.now()
|
|
50
|
+
terminal.color_print.info("Connecting to streaming server -> ", end="")
|
|
51
|
+
async with websockets.connect(self._streamer_info.get('streamerSocketUrl'), ping_interval=None) as self._websocket:
|
|
52
|
+
print("Connected.")
|
|
53
|
+
login_payload = self.basic_request(service="ADMIN", command="LOGIN", parameters={"Authorization": self.client.access_token, "SchwabClientChannel": self._streamer_info.get("schwabClientChannel"), "SchwabClientFunctionId": self._streamer_info.get("schwabClientFunctionId")})
|
|
54
|
+
await self._websocket.send(json.dumps(login_payload))
|
|
55
|
+
receiver_func(await self._websocket.recv())
|
|
56
|
+
self.active = True
|
|
57
|
+
# send queued requests
|
|
58
|
+
while self._queue:
|
|
59
|
+
await self._websocket.send(json.dumps({"requests": self._queue.pop(0)}))
|
|
60
|
+
receiver_func(await self._websocket.recv())
|
|
61
|
+
# TODO: resend requests if the stream crashes
|
|
62
|
+
while True:
|
|
63
|
+
receiver_func(await self._websocket.recv())
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.active = False
|
|
66
|
+
terminal.color_print.error(f"{e}")
|
|
67
|
+
if e is websockets.exceptions.ConnectionClosedOK:
|
|
68
|
+
terminal.color_print.info("Stream has closed.")
|
|
69
|
+
break
|
|
70
|
+
elif e is RuntimeError:
|
|
71
|
+
terminal.color_print.warning("Streaming window has closed.")
|
|
72
|
+
break
|
|
73
|
+
elif (datetime.now() - self._start_timestamp).seconds < 60:
|
|
74
|
+
terminal.color_print.error("Stream not alive for more than 1 minute, exiting...")
|
|
75
|
+
break
|
|
76
|
+
else:
|
|
77
|
+
terminal.color_print.warning("Connection lost to server, reconnecting...")
|
|
78
|
+
|
|
79
|
+
def start(self, receiver="default"):
|
|
80
|
+
def _start_async():
|
|
81
|
+
asyncio.run(self._start_streamer(receiver))
|
|
82
|
+
|
|
83
|
+
threading.Thread(target=_start_async).start()
|
|
84
|
+
sleep(4) # wait for thread/stream to start
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def start_automatic(self, after_hours=False, pre_hours=False):
|
|
88
|
+
start = time(9, 30, 0) # market opens at 9:30
|
|
89
|
+
end = time(16, 0, 0) # market closes at 4:00
|
|
90
|
+
if pre_hours:
|
|
91
|
+
start = time(8, 0, 0)
|
|
92
|
+
if after_hours:
|
|
93
|
+
end = time(20, 0, 0)
|
|
94
|
+
|
|
95
|
+
def checker():
|
|
96
|
+
|
|
97
|
+
while True:
|
|
98
|
+
in_hours = (start <= datetime.now().time() <= end) and (0 <= datetime.now().weekday() <= 4)
|
|
99
|
+
if in_hours and not self.active:
|
|
100
|
+
self.start()
|
|
101
|
+
elif not in_hours and self.active:
|
|
102
|
+
terminal.color_print.info("Stopping Stream.")
|
|
103
|
+
self.stop()
|
|
104
|
+
sleep(60)
|
|
105
|
+
|
|
106
|
+
threading.Thread(target=checker).start()
|
|
107
|
+
|
|
108
|
+
if not start <= datetime.now().time() <= end:
|
|
109
|
+
terminal.color_print.info("Stream was started outside of active hours and will launch when in hours.")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def send(self, requests):
|
|
113
|
+
async def _send(toSend):
|
|
114
|
+
await self._websocket.send(toSend)
|
|
115
|
+
if type(requests) is not list:
|
|
116
|
+
requests = [requests]
|
|
117
|
+
if self.active:
|
|
118
|
+
toSend = json.dumps({"requests": requests})
|
|
119
|
+
asyncio.run(_send(toSend))
|
|
120
|
+
else:
|
|
121
|
+
terminal.color_print.warning("Stream is not active, request queued.")
|
|
122
|
+
self._queue.append(requests)
|
|
123
|
+
|
|
124
|
+
# TODO: Fix this (wont properly close)
|
|
125
|
+
def stop(self):
|
|
126
|
+
self._request_id += 1
|
|
127
|
+
self.send(self.basic_request(service="ADMIN", command="LOGOUT"))
|
|
128
|
+
self.active = False
|
|
129
|
+
|
|
130
|
+
def basic_request(self, service, command, parameters=None):
|
|
131
|
+
if self._streamer_info is None:
|
|
132
|
+
response = self.client.preferences()
|
|
133
|
+
if response.ok:
|
|
134
|
+
self._streamer_info = response.json().get('streamerInfo', None)[0]
|
|
135
|
+
|
|
136
|
+
if self._streamer_info is not None:
|
|
137
|
+
request = {"service": service.upper(),
|
|
138
|
+
"command": command.upper(),
|
|
139
|
+
"requestid": self._request_id,
|
|
140
|
+
"SchwabClientCustomerId": self._streamer_info.get("schwabClientCustomerId"),
|
|
141
|
+
"SchwabClientCorrelId": self._streamer_info.get("schwabClientCorrelId")}
|
|
142
|
+
if parameters is not None: request["parameters"] = parameters
|
|
143
|
+
self._request_id += 1
|
|
144
|
+
return request
|
|
145
|
+
else:
|
|
146
|
+
terminal.color_print.error("Could not get streamerInfo")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _list_to_string(ls):
|
|
151
|
+
if type(ls) is str: return ls
|
|
152
|
+
elif type(ls) is list: return ",".join(map(str, ls))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# requests that can be sent to the stream
|
|
156
|
+
def chart_equity(self, keys, fields, command="SUBS"):
|
|
157
|
+
return self.basic_request("CHART_EQUITY", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
158
|
+
|
|
159
|
+
def chart_futures(self, keys, fields, command="SUBS"):
|
|
160
|
+
return self.basic_request("CHART_FUTURES", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
161
|
+
|
|
162
|
+
def level_one_quote(self, keys, fields, command="SUBS"): # Service not available or temporary down.
|
|
163
|
+
return self.basic_request("QUOTE", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
164
|
+
|
|
165
|
+
def level_one_option(self, keys, fields, command="SUBS"):
|
|
166
|
+
return self.basic_request("OPTION", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
167
|
+
|
|
168
|
+
def level_one_futures(self, keys, fields, command="SUBS"):
|
|
169
|
+
return self.basic_request("LEVELONE_FUTURES", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
170
|
+
|
|
171
|
+
def level_one_forex(self, keys, fields, command="SUBS"):
|
|
172
|
+
return self.basic_request("LEVELONE_FOREX", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
173
|
+
|
|
174
|
+
def level_one_futures_options(self, keys, fields, command="SUBS"):
|
|
175
|
+
return self.basic_request("LEVELONE_FUTURES_OPTIONS", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
class account:
|
|
181
|
+
@staticmethod
|
|
182
|
+
def activity(keys, fields, command="SUBS"):
|
|
183
|
+
return Stream.request(command, "ACCT_ACTIVITY", keys, fields)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class actives:
|
|
187
|
+
@staticmethod
|
|
188
|
+
def nasdaq(keys, fields, command="SUBS"):
|
|
189
|
+
return Stream.request(command, "ACTIVES_NASDAQ", keys, fields)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def nyse(keys, fields, command="SUBS"):
|
|
193
|
+
return Stream.request(command, "ACTIVES_NYSE", keys, fields)
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def otcbb(keys, fields, command="SUBS"):
|
|
197
|
+
return Stream.request(command, "ACTIVES_OTCBB", keys, fields)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def options(keys, fields, command="SUBS"):
|
|
201
|
+
return Stream.request(command, "ACTIVES_OPTIONS", keys, fields)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class book:
|
|
206
|
+
@staticmethod
|
|
207
|
+
def forex(keys, fields, command="SUBS"):
|
|
208
|
+
return Stream.request(command, "FOREX_BOOK", keys, fields)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def futures(keys, fields, command="SUBS"):
|
|
212
|
+
return Stream.request(command, "FUTURES_BOOK", keys, fields)
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def listed(keys, fields, command="SUBS"):
|
|
216
|
+
return Stream.request(command, "LISTED_BOOK", keys, fields)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def nasdaq(keys, fields, command="SUBS"):
|
|
220
|
+
return Stream.request(command, "NASDAQ_BOOK", keys, fields)
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def options(keys, fields, command="SUBS"):
|
|
224
|
+
return Stream.request(command, "OPTIONS_BOOK", keys, fields)
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def futures_options(keys, fields, command="SUBS"):
|
|
228
|
+
return Stream.request(command, "FUTURES_OPTIONS_BOOK", keys, fields)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class levelTwo:
|
|
232
|
+
@staticmethod
|
|
233
|
+
def _NA():
|
|
234
|
+
print("Not Available")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class news:
|
|
238
|
+
@staticmethod
|
|
239
|
+
def headline(keys, fields, command="SUBS"):
|
|
240
|
+
return Stream.request(command, "NEWS_HEADLINE", keys, fields)
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def headlineList(keys, fields, command="SUBS"):
|
|
244
|
+
return Stream.request(command, "NEWS_HEADLINELIST", keys, fields)
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def headlineStory(keys, fields, command="SUBS"):
|
|
248
|
+
return Stream.request(command, "NEWS_STORY", keys, fields)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class timeSale:
|
|
252
|
+
@staticmethod
|
|
253
|
+
def equity(keys, fields, command="SUBS"):
|
|
254
|
+
return Stream.request(command, "TIMESALE_EQUITY", keys, fields)
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def forex(keys, fields, command="SUBS"):
|
|
258
|
+
return Stream.request(command, "TIMESALE_FOREX", keys, fields)
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def futures(keys, fields, command="SUBS"):
|
|
262
|
+
return Stream.request(command, "TIMESALE_FUTURES", keys, fields)
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def options(keys, fields, command="SUBS"):
|
|
266
|
+
return Stream.request(command, "TIMESALE_OPTIONS", keys, fields)
|
|
267
|
+
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
def _streamResponseHandler(streamOut):
|
|
273
|
+
try:
|
|
274
|
+
parentDict = json.loads(streamOut)
|
|
275
|
+
for key in parentDict.keys():
|
|
276
|
+
match key:
|
|
277
|
+
case "notify":
|
|
278
|
+
self._terminal.print(
|
|
279
|
+
f"[Heartbeat]: {Stream.epochMSToDate(parentDict['notify'][0]['heartbeat'])}")
|
|
280
|
+
case "response":
|
|
281
|
+
for resp in parentDict.get('response'):
|
|
282
|
+
self._terminal.print(f"[Response]: {resp}")
|
|
283
|
+
case "snapshot":
|
|
284
|
+
for snap in parentDict.get('snapshot'):
|
|
285
|
+
self._terminal.print(f"[Snapshot]: {snap}")
|
|
286
|
+
case "data":
|
|
287
|
+
for data in parentDict.get("data"):
|
|
288
|
+
if data.get('service').upper() in universe.streamFieldAliases:
|
|
289
|
+
service = data.get("service")
|
|
290
|
+
timestamp = data.get("timestamp")
|
|
291
|
+
for symbolData in data.get("content"):
|
|
292
|
+
tempSnapshot = database.Snapshot(service, symbolData.get("key"), timestamp, symbolData)
|
|
293
|
+
if universe.preferences.usingDatabase:
|
|
294
|
+
database.DBAddSnapshot(tempSnapshot) # add to database
|
|
295
|
+
if universe.preferences.usingDataframes:
|
|
296
|
+
database.DFAddSnapshot(tempSnapshot) # add to dataframes
|
|
297
|
+
self._terminal.print(
|
|
298
|
+
f"[Data]: {tempSnapshot.toPrettyString()}") # to stream output
|
|
299
|
+
case _:
|
|
300
|
+
self._terminal.print(f"[Unknown Response]: {streamOut}")
|
|
301
|
+
except Exception as e:
|
|
302
|
+
self._terminal.print(f"[ERROR]: There was an error in decoding the stream response: {streamOut}")
|
|
303
|
+
self._terminal.print(f"[ERROR]: The error was: {e}")
|
|
304
|
+
"""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file is used to print colored text and create multiple terminals
|
|
3
|
+
Github: https://github.com/tylerebowers/Schwab-API-Python
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class color_print:
|
|
8
|
+
@staticmethod
|
|
9
|
+
def info(string, end="\n"): print(f"\033[92m{'[INFO]: '}\033[00m{string}", end=end)
|
|
10
|
+
@staticmethod
|
|
11
|
+
def warning(string, end="\n"): print(f"\033[93m{'[WARN]: '}\033[00m{string}", end=end)
|
|
12
|
+
@staticmethod
|
|
13
|
+
def error(string, end="\n"): print(f"\033[91m{'[ERROR]: '}\033[00m{string}", end=end)
|
|
14
|
+
@staticmethod
|
|
15
|
+
def user(string, end="\n"): print(f"\033[94m{'[USER]: '}\033[00m{string}", end=end)
|
|
16
|
+
@staticmethod
|
|
17
|
+
def input(string): return input(f"\033[94m{'[INPUT]: '}\033[00m{string}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
from time import sleep
|
|
21
|
+
import tkinter as tk
|
|
22
|
+
from tkinter import ttk
|
|
23
|
+
import threading
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class multiTerminal(threading.Thread):
|
|
27
|
+
|
|
28
|
+
def __init__(self, title="Terminal", height=20, width=200, font=("Courier New", "12"), backgroundColor="gray5", textColor="snow", allowClosing=True, ignoreClosedPrints=True):
|
|
29
|
+
#params
|
|
30
|
+
self.title = title
|
|
31
|
+
self.height = height
|
|
32
|
+
self.width = width
|
|
33
|
+
self.font = font
|
|
34
|
+
self.backgroundColor = backgroundColor
|
|
35
|
+
self.textColor = textColor
|
|
36
|
+
self.allowClosing = allowClosing
|
|
37
|
+
self.ignoreClosedPrints = ignoreClosedPrints
|
|
38
|
+
#internal variables
|
|
39
|
+
self._root = None # main window
|
|
40
|
+
self._tb = None # text box
|
|
41
|
+
self.isOpen = False # if the window is open
|
|
42
|
+
self._safeExit = False # if the window is safe to exit
|
|
43
|
+
threading.Thread.__init__(self, daemon=True) # kill child thread on main thread exit
|
|
44
|
+
self.start()
|
|
45
|
+
sleep(0.5) # wait for window to open
|
|
46
|
+
|
|
47
|
+
def close(self):
|
|
48
|
+
if self.isOpen and self._safeExit: # safeExit is True is no operations are being done (i.e. print)
|
|
49
|
+
self.isOpen = False
|
|
50
|
+
self._root.quit()
|
|
51
|
+
self._root.update()
|
|
52
|
+
self._root = None
|
|
53
|
+
|
|
54
|
+
def run(self):
|
|
55
|
+
self._root = tk.Tk()
|
|
56
|
+
if self.allowClosing: self._root.protocol("WM_DELETE_WINDOW", self.close)
|
|
57
|
+
else: self._root.protocol("WM_DELETE_WINDOW", lambda: None)
|
|
58
|
+
self._root.title(self.title)
|
|
59
|
+
self._tb = tk.Text(self._root, height=self.height, width=self.width, wrap="none", font=self.font)
|
|
60
|
+
self._tb.pack(side="left", fill="both", expand=True)
|
|
61
|
+
self._tb.configure(state="disabled", bg=self.backgroundColor, fg=self.textColor)
|
|
62
|
+
sizegrip = ttk.Sizegrip(self._tb)
|
|
63
|
+
sizegrip.configure(cursor="sizing")
|
|
64
|
+
sizegrip.bind("<1>", self._resize_start)
|
|
65
|
+
sizegrip.bind("<B1-Motion>", self._resize_update)
|
|
66
|
+
self.isOpen = True
|
|
67
|
+
self._safeExit = True
|
|
68
|
+
self._root.mainloop()
|
|
69
|
+
|
|
70
|
+
def print(self, toPrint, end="\n"):
|
|
71
|
+
if self._root is None or not self.isOpen:
|
|
72
|
+
if not self.ignoreClosedPrints: return print(f"Terminal \"{self.title}\" is closed")
|
|
73
|
+
#if not self.ignoreClosedPrints: raise Exception(f"Terminal \"{self.title}\" is closed")
|
|
74
|
+
else:
|
|
75
|
+
self._safeExit = False # needed so that we don't kill mainloop while printing
|
|
76
|
+
self._tb.configure(state="normal")
|
|
77
|
+
self._tb.insert("end", f"{toPrint}{end}")
|
|
78
|
+
self._tb.see("end")
|
|
79
|
+
self._tb.configure(state="disabled")
|
|
80
|
+
self._safeExit = True
|
|
81
|
+
|
|
82
|
+
def _resize_start(self, event):
|
|
83
|
+
self._x = event.x
|
|
84
|
+
self._y = event.y
|
|
85
|
+
|
|
86
|
+
def _resize_update(self, event):
|
|
87
|
+
delta_x = event.x - self._x
|
|
88
|
+
delta_y = event.y - self._y
|
|
89
|
+
self._tb.place_configure(width=self._tb.winfo_width() + delta_x, height=self._tb.winfo_height() + delta_y)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: schwabdev
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Schwab API Python Client (unofficial)
|
|
5
|
+
Author: Tyler Bowers
|
|
6
|
+
Author-email: tylerebowers@gmail.com
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Source, https://github.com/tylerebowers/Schwab-API-Python
|
|
9
|
+
Project-URL: Youtube, https://www.youtube.com/playlist?list=PLs4JLWxBQIxpbvCj__DjAc0RRTlBz-TR8
|
|
10
|
+
Project-URL: PyPI, https://pypi.org/project/schwabdev/
|
|
11
|
+
Keywords: python,schwab,api,client,finance,trading,stocks,equities,options,forex,futures
|
|
12
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Schwab-API-Python
|
|
22
|
+
This is an unofficial python program to access the Schwab api.
|
|
23
|
+
You will need a Schwab developer account [here](https://beta-developer.schwab.com/).
|
|
24
|
+
Join the [Discord group](https://discord.gg/m7SSjr9rs9).
|
|
25
|
+
Also found on [PyPI](https://pypi.org/project/schwabdev/), install via `pip3 install schwabdev`
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Quick setup
|
|
29
|
+
1. Create a new Schwab individual developer app with callback url "https://127.0.0.1" (case sensitive) and wait until the status is "Ready for use", note that "Approved - Pending" will not work.
|
|
30
|
+
2. Enable TOS (Thinkorswim) for your Schwab account, it is needed for orders and other api calls.
|
|
31
|
+
3. Python version 3.11 or higher is required.
|
|
32
|
+
4. `pip3 install schwabdev requests websockets tk` (tkinter/tk may need to be installed differently)
|
|
33
|
+
5. Import the package `import schwabdev`
|
|
34
|
+
6. Create a client `client = schwabdev.Client('Your app key', 'Your app secret')`
|
|
35
|
+
7. Examples on how to use the client are in `tests/api_demo.py`
|
|
36
|
+
|
|
37
|
+
## What can this program do?
|
|
38
|
+
- Authenticate and access the api
|
|
39
|
+
- Functions for all api functions (examples in `tests/api_demo.py`)
|
|
40
|
+
- Auto "access token" updates (`client.update_tokens_auto()`)
|
|
41
|
+
- Stream real-time data with customizable response handler (examples in `tests/stream_demo.py`)
|
|
42
|
+
### TBD
|
|
43
|
+
- Automatic refresh token updates. (Waiting for Schwab implementation)
|
|
44
|
+
|
|
45
|
+
## Notes
|
|
46
|
+
|
|
47
|
+
The schwabdev folder contains code for main operations:
|
|
48
|
+
- `api.py` contains functions relating to api calls, requests, and automatic token checker threads.
|
|
49
|
+
- `stream.py` contains functions for streaming data from websockets.
|
|
50
|
+
- `terminal.py` contains a program for making additional terminal windows and printing to the terminal with color.
|
|
51
|
+
|
|
52
|
+
## License (MIT)
|
|
53
|
+
|
|
54
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
55
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
56
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
57
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
58
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
59
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
60
|
+
SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
schwabdev/__init__.py
|
|
4
|
+
schwabdev/api.py
|
|
5
|
+
schwabdev/stream.py
|
|
6
|
+
schwabdev/terminal.py
|
|
7
|
+
schwabdev.egg-info/PKG-INFO
|
|
8
|
+
schwabdev.egg-info/SOURCES.txt
|
|
9
|
+
schwabdev.egg-info/dependency_links.txt
|
|
10
|
+
schwabdev.egg-info/requires.txt
|
|
11
|
+
schwabdev.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
schwabdev
|
schwabdev-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
VERSION = '1.0.0'
|
|
4
|
+
DESCRIPTION = 'Schwab API Python Client (unofficial)'
|
|
5
|
+
with open('README.md', 'r') as f:
|
|
6
|
+
LONG_DESCRIPTION = f.read()
|
|
7
|
+
|
|
8
|
+
setup(
|
|
9
|
+
name='schwabdev',
|
|
10
|
+
version=VERSION,
|
|
11
|
+
author='Tyler Bowers',
|
|
12
|
+
author_email='tylerebowers@gmail.com',
|
|
13
|
+
license='MIT',
|
|
14
|
+
description=DESCRIPTION,
|
|
15
|
+
long_description=LONG_DESCRIPTION,
|
|
16
|
+
long_description_content_type='text/markdown',
|
|
17
|
+
packages=find_packages(),
|
|
18
|
+
python_requires='>=3.11',
|
|
19
|
+
install_requires=[
|
|
20
|
+
'requests',
|
|
21
|
+
'websockets',
|
|
22
|
+
],
|
|
23
|
+
keywords=['python', 'schwab', 'api', 'client', 'finance', 'trading', 'stocks', 'equities', 'options', 'forex', 'futures'],
|
|
24
|
+
classifiers=[
|
|
25
|
+
'Topic :: Office/Business :: Financial :: Investment',
|
|
26
|
+
'License :: OSI Approved :: MIT License',
|
|
27
|
+
'Programming Language :: Python :: 3',
|
|
28
|
+
'Operating System :: OS Independent',
|
|
29
|
+
'Intended Audience :: Developers',
|
|
30
|
+
'Natural Language :: English',
|
|
31
|
+
],
|
|
32
|
+
project_urls={
|
|
33
|
+
'Source': 'https://github.com/tylerebowers/Schwab-API-Python',
|
|
34
|
+
'Youtube': 'https://www.youtube.com/playlist?list=PLs4JLWxBQIxpbvCj__DjAc0RRTlBz-TR8',
|
|
35
|
+
'PyPI': 'https://pypi.org/project/schwabdev/'
|
|
36
|
+
}
|
|
37
|
+
)
|