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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: schwabdev
3
- Version: 2.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 requests websockets`
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 requests websockets`
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. 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.
44
- - Questions? - join the [Discord group](https://discord.gg/m7SSjr9rs9).
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 requests websockets`
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 requests websockets`
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. 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.
20
- - Questions? - join the [Discord group](https://discord.gg/m7SSjr9rs9).
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
 
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
@@ -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._app_key = app_key # app key credential
52
- self._app_secret = app_secret # app secret credential
53
- self._callback_url = callback_url # callback url to use
54
- self.access_token = None # access token from auth
55
- self.refresh_token = None # refresh token from auth
56
- self.id_token = None # id token from auth
57
- self._access_token_issued = None # datetime of access token issue
58
- self._refresh_token_issued = None # datetime of refresh token issue
59
- self._access_token_timeout = 1800 # in seconds (from schwab)
60
- self._refresh_token_timeout = 7 # in days (from schwab)
61
- self._tokens_file = tokens_file # path to tokens file
62
- self.timeout = timeout # timeout to use in requests
63
- self.verbose = verbose # verbose mode
64
- self.stream = Stream(self) # init the streaming object
65
- self.awaiting_input = False # whether we are awaiting user input
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
- print(self._access_token_issued.strftime("Access token last updated: %Y-%m-%d %H:%M:%S") + f" (expires in {self._access_token_timeout - (datetime.datetime.now(datetime.timezone.utc) - self._access_token_issued).seconds} seconds)")
78
- print(self._refresh_token_issued.strftime("Refresh token last updated: %Y-%m-%d %H:%M:%S") + f" (expires in {self._refresh_token_timeout - (datetime.datetime.now(datetime.timezone.utc) - self._refresh_token_issued).days} days)")
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(60)
104
+ time.sleep(30)
95
105
  threading.Thread(target=checker, daemon=True).start()
96
- elif not self.verbose:
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
- if (datetime.datetime.now(datetime.timezone.utc) - self._refresh_token_issued).days >= (self._refresh_token_timeout - 1) or force: # check if we need to update refresh (and access) token
109
- print("The refresh token has expired, please update!")
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).days >= 1) or (
112
- (datetime.datetime.now(datetime.timezone.utc) - self._access_token_issued).seconds > (self._access_token_timeout - 61)): # check if we need to update access token
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: 'datetime | str', toEnteredTime: 'datetime | str', maxResults=None, status=None) -> requests.Response:
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: 'datetime | str', toEnteredTime: 'datetime | str', maxResults=None, status=None) -> requests.Response:
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: 'datetime | str', endDate: 'datetime | str', types: str, symbol=None) -> requests.Response:
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=None, fields=None, indicative=False) -> requests.Response:
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: list of fields to get ("all", "quote", "fundamental")
497
- :type fields: list
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: list of fields to get ("all", "quote", "fundamental")
515
- :type fields: list
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.quote(symbol_id)}/quotes',
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, *args, **kwargs):
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(), *args, **kwargs)
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(), *args, **kwargs)
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(), *args, **kwargs)
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 has closed.")
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
- if self.verbose: print(f"Stream closed (likely no subscriptions): {e}")
94
+ print(f"[Schwabdev] Stream connection closed (likely no subscriptions): {e}")
94
95
  break
95
- elif (datetime.now() - start_time).seconds <= 90:
96
- if self.verbose: print("Stream has crashed within 90 seconds, likely no subscriptions or invalid login.")
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
- if self.verbose: print(f"{e}")
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, *args, **kwargs):
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, *args, **kwargs))
112
+ asyncio.run(self._start_streamer(receiver, **kwargs))
111
113
 
112
- self._thread = threading.Thread(target=_start_async, daemon=False)
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 start_automatic(self, receiver=print, after_hours=False, pre_hours=False):
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(9, 29, 0) # market opens at 9:30
129
- end = time(16, 0, 0) # market closes at 4:00
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(7, 59, 0)
133
+ start = datetime.time(10, 59, 0, tzinfo=datetime.timezone.utc)
132
134
  if after_hours:
133
- end = time(20, 0, 0)
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
- in_hours = (start <= datetime.now().time() <= end) and (0 <= datetime.now().weekday() <= 4)
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(60)
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 stop(self, clear_subscriptions=True):
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
- if self._streamer_info is not None:
252
- request = {"service": service.upper(),
253
- "command": command.upper(),
254
- "requestid": self._request_id,
255
- "SchwabClientCustomerId": self._streamer_info.get("schwabClientCustomerId"),
256
- "SchwabClientCorrelId": self._streamer_info.get("schwabClientCorrelId")}
257
- if parameters is not None and len(parameters) > 0: request["parameters"] = parameters
258
- self._request_id += 1
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.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 requests websockets`
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 requests websockets`
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. 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.
44
- - Questions? - join the [Discord group](https://discord.gg/m7SSjr9rs9).
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
 
@@ -1,5 +1,6 @@
1
1
  LICENSE.txt
2
2
  README.md
3
+ pyproject.toml
3
4
  setup.py
4
5
  schwabdev/__init__.py
5
6
  schwabdev/api.py
@@ -1,6 +1,6 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
- VERSION = '2.2.2'
3
+ VERSION = '2.2.4' # Also update version in api.py
4
4
  DESCRIPTION = 'An easy and lightweight wrapper for using the Charles Schwab API.'
5
5
  with open('README.md', 'r') as f:
6
6
  LONG_DESCRIPTION = f.read()
File without changes
File without changes