schwabdev 2.2.0__tar.gz → 2.2.2__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.0
3
+ Version: 2.2.2
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
@@ -19,6 +19,8 @@ Classifier: Natural Language :: English
19
19
  Requires-Python: >=3.11
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE.txt
22
+ Requires-Dist: requests
23
+ Requires-Dist: websockets
22
24
 
23
25
  # Schwab-API-Python
24
26
  ![PyPI - Version](https://img.shields.io/pypi/v/schwabdev) ![Discord](https://img.shields.io/discord/1076596998150561873?logo=discord) ![PyPI - Downloads](https://img.shields.io/pypi/dm/schwabdev) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate/?business=8VDFKHMBFSC2Q&no_recurring=0&currency_code=USD) ![YouTube Video Views](https://img.shields.io/youtube/views/kHbom0KIJwc?style=flat&logo=youtube)
@@ -54,6 +56,7 @@ print(client.account_linked().json()) #make api calls
54
56
  - Functions for all api functions (examples in `examples/api_demo.py`)
55
57
  - Stream real-time data with a customizable response handler (examples in `examples/stream_demo.py`)
56
58
  ### TBD
59
+ - Paper trading client
57
60
  - Automatic refresh token updates. (Waiting for Schwab implementation)
58
61
  ### Notes
59
62
  The schwabdev folder contains code for main operations:
@@ -32,6 +32,7 @@ print(client.account_linked().json()) #make api calls
32
32
  - Functions for all api functions (examples in `examples/api_demo.py`)
33
33
  - Stream real-time data with a customizable response handler (examples in `examples/stream_demo.py`)
34
34
  ### TBD
35
+ - Paper trading client
35
36
  - Automatic refresh token updates. (Waiting for Schwab implementation)
36
37
  ### Notes
37
38
  The schwabdev folder contains code for main operations:
@@ -1,13 +1,12 @@
1
1
  import json
2
2
  import time
3
3
  import base64
4
+ import datetime
4
5
  import requests
5
6
  import threading
6
7
  import webbrowser
7
8
  import urllib.parse
8
9
  from .stream import Stream
9
- from datetime import datetime
10
-
11
10
 
12
11
  class Client:
13
12
 
@@ -75,8 +74,8 @@ class Client:
75
74
  self._access_token_issued = at_issued
76
75
  self._refresh_token_issued = rt_issued
77
76
  if self.verbose:
78
- print(self._access_token_issued.strftime("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)")
79
- print(self._refresh_token_issued.strftime("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)")
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)")
80
79
  # check if tokens need to be updated and update if needed
81
80
  self.update_tokens()
82
81
  else:
@@ -98,7 +97,7 @@ class Client:
98
97
  print("Warning: Tokens will not be updated automatically.")
99
98
 
100
99
  if self.verbose:
101
- print("Initialization Complete")
100
+ print("Schwabdev Client Initialization Complete")
102
101
 
103
102
  def update_tokens(self, force=False):
104
103
  """
@@ -106,11 +105,11 @@ class Client:
106
105
  :param force: force update of refresh token (also updates access token)
107
106
  :type force: bool
108
107
  """
109
- if (datetime.now() - self._refresh_token_issued).days >= (self._refresh_token_timeout - 1) or force: # check if we need to update refresh (and access) token
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
110
109
  print("The refresh token has expired, please update!")
111
110
  self._update_refresh_token()
112
- elif ((datetime.now() - self._access_token_issued).days >= 1) or (
113
- (datetime.now() - self._access_token_issued).seconds > (self._access_token_timeout - 61)): # check if we need to update access 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
114
113
  if self.verbose: print("The access token has expired, updating automatically.")
115
114
  self._update_access_token()
116
115
 
@@ -129,7 +128,7 @@ class Client:
129
128
  response = self._post_oauth_token('refresh_token', token_dictionary_old.get("refresh_token"))
130
129
  if response.ok:
131
130
  # get and update to the new access token
132
- self._access_token_issued = datetime.now()
131
+ self._access_token_issued = datetime.datetime.now(datetime.timezone.utc)
133
132
  self._refresh_token_issued = refresh_token_issued
134
133
  new_td = response.json()
135
134
  self.access_token = new_td.get("access_token")
@@ -140,6 +139,7 @@ class Client:
140
139
  print(f"Access token updated: {self._access_token_issued}")
141
140
  break
142
141
  else:
142
+ print(response.text)
143
143
  print(f"Could not get new access token ({i+1} of 3).")
144
144
  time.sleep(10)
145
145
 
@@ -159,7 +159,7 @@ class Client:
159
159
  response = self._post_oauth_token('authorization_code', code)
160
160
  if response.ok:
161
161
  # update token file and variables
162
- self._access_token_issued = self._refresh_token_issued = datetime.now()
162
+ self._access_token_issued = self._refresh_token_issued = datetime.datetime.now(datetime.timezone.utc)
163
163
  new_td = response.json()
164
164
  self.access_token = new_td.get("access_token")
165
165
  self.refresh_token = new_td.get("refresh_token")
@@ -168,6 +168,7 @@ class Client:
168
168
  self._write_tokens_file(self._access_token_issued, self._refresh_token_issued, new_td)
169
169
  if self.verbose: print("Refresh and Access tokens updated")
170
170
  else:
171
+ print(response.text)
171
172
  print("Could not get new refresh and access tokens, check these:\n 1. App status is "
172
173
  "\"Ready For Use\".\n 2. App key and app secret are valid.\n 3. You pasted the "
173
174
  "whole url within 30 seconds. (it has a quick expiration)")
@@ -198,9 +199,9 @@ class Client:
198
199
  """
199
200
  Writes token file
200
201
  :param at_issued: access token issued
201
- :type at_issued: datetime
202
+ :type at_issued: datetime.pyi
202
203
  :param rt_issued: refresh token issued
203
- :type rt_issued: datetime
204
+ :type rt_issued: datetime.pyi
204
205
  :param token_dictionary: token dictionary
205
206
  :type token_dictionary: dict
206
207
  """
@@ -218,12 +219,12 @@ class Client:
218
219
  """
219
220
  Reads token file
220
221
  :return: access token issued, refresh token issued, token dictionary
221
- :rtype: datetime, datetime, dict
222
+ :rtype: datetime.pyi, datetime.pyi, dict
222
223
  """
223
224
  try:
224
225
  with open(self._tokens_file, 'r') as f:
225
226
  d = json.load(f)
226
- return datetime.fromisoformat(d.get("access_token_issued")), datetime.fromisoformat(d.get("refresh_token_issued")), d.get("token_dictionary")
227
+ return datetime.datetime.fromisoformat(d.get("access_token_issued")), datetime.datetime.fromisoformat(d.get("refresh_token_issued")), d.get("token_dictionary")
227
228
  except Exception as e:
228
229
  print(e)
229
230
  return None, None, None
@@ -243,8 +244,8 @@ class Client:
243
244
  def _time_convert(self, dt=None, form="8601"):
244
245
  """
245
246
  Convert time to the correct format, passthrough if a string, preserve None if None for params parser
246
- :param dt: datetime object to convert
247
- :type dt: datetime
247
+ :param dt: datetime.pyi object to convert
248
+ :type dt: datetime.pyi
248
249
  :param form: what to convert input to
249
250
  :type form: str
250
251
  :return: converted time or passthrough
@@ -253,7 +254,7 @@ class Client:
253
254
  if dt is None or isinstance(dt, str):
254
255
  return dt
255
256
  elif form == "8601": # assume datetime object from here on
256
- return f'{dt.isoformat()[:-3]}Z'
257
+ return f'{dt.isoformat()[:-9]}Z'
257
258
  elif form == "epoch":
258
259
  return int(dt.timestamp())
259
260
  elif form == "epoch_ms":
@@ -263,7 +264,7 @@ class Client:
263
264
  else:
264
265
  return dt
265
266
 
266
- def _format_list(self, l):
267
+ def _format_list(self, l: list | str | None):
267
268
  """
268
269
  Convert python list to string or passthough if already a string i.e ["a", "b"] -> "a,b"
269
270
  :param l: list to convert
@@ -284,7 +285,7 @@ class Client:
284
285
  Accounts and Trading Production
285
286
  """
286
287
 
287
- def account_linked(self):
288
+ def account_linked(self) -> requests.Response:
288
289
  """
289
290
  Account numbers in plain text cannot be used outside of headers or request/response bodies. As the first step consumers must invoke this service to retrieve the list of plain text/encrypted value pairs, and use encrypted account values for all subsequent calls for any accountNumber request.
290
291
  :return: All linked account numbers and hashes
@@ -294,7 +295,7 @@ class Client:
294
295
  headers={'Authorization': f'Bearer {self.access_token}'},
295
296
  timeout=self.timeout)
296
297
 
297
- def account_details_all(self, fields=None):
298
+ def account_details_all(self, fields=None) -> requests.Response:
298
299
  """
299
300
  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.
300
301
  :param fields: fields to return (options: "positions")
@@ -307,7 +308,7 @@ class Client:
307
308
  params=self._params_parser({'fields': fields}),
308
309
  timeout=self.timeout)
309
310
 
310
- def account_details(self, accountHash, fields=None):
311
+ def account_details(self, accountHash: str, fields=None) -> requests.Response:
311
312
  """
312
313
  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.
313
314
  :param accountHash: account hash from account_linked()
@@ -322,15 +323,15 @@ class Client:
322
323
  params=self._params_parser({'fields': fields}),
323
324
  timeout=self.timeout)
324
325
 
325
- def account_orders(self, accountHash, fromEnteredTime, toEnteredTime, maxResults=None, status=None):
326
+ def account_orders(self, accountHash: str, fromEnteredTime: 'datetime | str', toEnteredTime: 'datetime | str', maxResults=None, status=None) -> requests.Response:
326
327
  """
327
328
  All orders for a specific account. Orders retrieved can be filtered based on input parameters below. Maximum date range is 1 year.
328
329
  :param accountHash: account hash from account_linked()
329
330
  :type accountHash: str
330
331
  :param fromEnteredTime: from entered time
331
- :type fromEnteredTime: datetime | str
332
+ :type fromEnteredTime: datetime.pyi | str
332
333
  :param toEnteredTime: to entered time
333
- :type toEnteredTime: datetime | str
334
+ :type toEnteredTime: datetime.pyi | str
334
335
  :param maxResults: maximum number of results
335
336
  :type maxResults: int
336
337
  :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")
@@ -345,7 +346,7 @@ class Client:
345
346
  'toEnteredTime': self._time_convert(toEnteredTime, "8601"), 'status': status}),
346
347
  timeout=self.timeout)
347
348
 
348
- def order_place(self, accountHash, order):
349
+ def order_place(self, accountHash: str, order: dict) -> requests.Response:
349
350
  """
350
351
  Place an order for a specific account.
351
352
  :param accountHash: account hash from account_linked()
@@ -361,13 +362,13 @@ class Client:
361
362
  json=order,
362
363
  timeout=self.timeout)
363
364
 
364
- def order_details(self, accountHash, orderId):
365
+ def order_details(self, accountHash:str, orderId: int | str) -> requests.Response:
365
366
  """
366
367
  Get a specific order by its ID, for a specific account
367
368
  :param accountHash: account hash from account_linked()
368
369
  :type accountHash: str
369
370
  :param orderId: order id
370
- :type orderId: str
371
+ :type orderId: int | str
371
372
  :return: order details
372
373
  :rtype: request.Response
373
374
  """
@@ -375,13 +376,13 @@ class Client:
375
376
  headers={'Authorization': f'Bearer {self.access_token}'},
376
377
  timeout=self.timeout)
377
378
 
378
- def order_cancel(self, accountHash, orderId):
379
+ def order_cancel(self, accountHash: str, orderId: int | str) -> requests.Response:
379
380
  """
380
381
  Cancel a specific order by its ID, for a specific account
381
382
  :param accountHash: account hash from account_linked()
382
383
  :type accountHash: str
383
384
  :param orderId: order id
384
- :type orderId: str
385
+ :type orderId: str|int
385
386
  :return: response code
386
387
  :rtype: request.Response
387
388
  """
@@ -389,13 +390,13 @@ class Client:
389
390
  headers={'Authorization': f'Bearer {self.access_token}'},
390
391
  timeout=self.timeout)
391
392
 
392
- def order_replace(self, accountHash, orderId, order):
393
+ def order_replace(self, accountHash: str, orderId: int | str, order: dict) -> requests.Response:
393
394
  """
394
395
  Replace an existing order for an account. The existing order will be replaced by the new order. Once replaced, the old order will be canceled and a new order will be created.
395
396
  :param accountHash: account hash from account_linked()
396
397
  :type accountHash: str
397
398
  :param orderId: order id
398
- :type orderId: str
399
+ :type orderId: str|int
399
400
  :param order: order dictionary, examples in Schwab docs
400
401
  :type order: dict
401
402
  :return: response code
@@ -407,13 +408,13 @@ class Client:
407
408
  json=order,
408
409
  timeout=self.timeout)
409
410
 
410
- def account_orders_all(self, fromEnteredTime, toEnteredTime, maxResults=None, status=None):
411
+ def account_orders_all(self, fromEnteredTime: 'datetime | str', toEnteredTime: 'datetime | str', maxResults=None, status=None) -> requests.Response:
411
412
  """
412
413
  Get all orders for all accounts
413
414
  :param fromEnteredTime: start date
414
- :type fromEnteredTime: datetime | str
415
+ :type fromEnteredTime: datetime.pyi | str
415
416
  :param toEnteredTime: end date
416
- :type toEnteredTime: datetime | str
417
+ :type toEnteredTime: datetime.pyi | str
417
418
  :param maxResults: maximum number of results (set to None for default 3000)
418
419
  :type maxResults: int
419
420
  :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")
@@ -429,22 +430,22 @@ class Client:
429
430
  timeout=self.timeout)
430
431
 
431
432
  """
432
- def order_preview(self, accountHash, orderObject):
433
+ def order_preview(self, accountHash, orderObject) -> requests.Response:
433
434
  #COMING SOON (waiting on Schwab)
434
435
  return requests.post(f'{self._base_api_url}/trader/v1/accounts/{accountHash}/previewOrder',
435
436
  headers={'Authorization': f'Bearer {self.access_token}',
436
437
  "Content-Type": "application.json"}, data=orderObject)
437
438
  """
438
439
 
439
- def transactions(self, accountHash, startDate, endDate, types, symbol=None):
440
+ def transactions(self, accountHash: str, startDate: 'datetime | str', endDate: 'datetime | str', types: str, symbol=None) -> requests.Response:
440
441
  """
441
442
  All transactions for a specific account. Maximum number of transactions in response is 3000. Maximum date range is 1 year.
442
443
  :param accountHash: account hash number
443
444
  :type accountHash: str
444
445
  :param startDate: start date
445
- :type startDate: datetime | str
446
+ :type startDate: datetime.pyi | str
446
447
  :param endDate: end date
447
- :type endDate: datetime | str
448
+ :type endDate: datetime.pyi | str
448
449
  :param types: transaction type ("TRADE, RECEIVE_AND_DELIVER, DIVIDEND_OR_INTEREST, ACH_RECEIPT, ACH_DISBURSEMENT, CASH_RECEIPT, CASH_DISBURSEMENT, ELECTRONIC_FUND, WIRE_OUT, WIRE_IN, JOURNAL, MEMORANDUM, MARGIN_CALL, MONEY_MARKET, SMA_ADJUSTMENT")
449
450
  :type types: str
450
451
  :param symbol: symbol
@@ -458,13 +459,13 @@ class Client:
458
459
  'endDate': self._time_convert(endDate, "8601"), 'symbol': symbol, 'types': types}),
459
460
  timeout=self.timeout)
460
461
 
461
- def transaction_details(self, accountHash, transactionId):
462
+ def transaction_details(self, accountHash: str, transactionId: str | int) -> requests.Response:
462
463
  """
463
464
  Get specific transaction information for a specific account
464
465
  :param accountHash: account hash number
465
466
  :type accountHash: str
466
467
  :param transactionId: transaction id
467
- :type transactionId: int
468
+ :type transactionId: str|int
468
469
  :return: transaction details of transaction id using accountHash
469
470
  :rtype: request.Response
470
471
  """
@@ -473,7 +474,7 @@ class Client:
473
474
  params={'accountNumber': accountHash, 'transactionId': transactionId},
474
475
  timeout=self.timeout)
475
476
 
476
- def preferences(self):
477
+ def preferences(self) -> requests.Response:
477
478
  """
478
479
  Get user preference information for the logged in user.
479
480
  :return: User Preferences and Streaming Info
@@ -487,7 +488,7 @@ class Client:
487
488
  Market Data
488
489
  """
489
490
 
490
- def quotes(self, symbols=None, fields=None, indicative=False):
491
+ def quotes(self, symbols=None, fields=None, indicative=False) -> requests.Response:
491
492
  """
492
493
  Get quotes for a list of tickers
493
494
  :param symbols: list of symbols strings (e.g. "AMD,INTC" or ["AMD", "INTC"])
@@ -505,7 +506,7 @@ class Client:
505
506
  {'symbols': self._format_list(symbols), 'fields': fields, 'indicative': indicative}),
506
507
  timeout=self.timeout)
507
508
 
508
- def quote(self, symbol_id, fields=None):
509
+ def quote(self, symbol_id: str, fields=None) -> requests.Response:
509
510
  """
510
511
  Get quote for a single symbol
511
512
  :param symbol_id: ticker symbol
@@ -520,9 +521,9 @@ class Client:
520
521
  params=self._params_parser({'fields': fields}),
521
522
  timeout=self.timeout)
522
523
 
523
- def option_chains(self, symbol, contractType=None, strikeCount=None, includeUnderlyingQuote=None, strategy=None,
524
+ def option_chains(self, symbol: str, contractType=None, strikeCount=None, includeUnderlyingQuote=None, strategy=None,
524
525
  interval=None, strike=None, range=None, fromDate=None, toDate=None, volatility=None, underlyingPrice=None,
525
- interestRate=None, daysToExpiration=None, expMonth=None, optionType=None, entitlement=None):
526
+ interestRate=None, daysToExpiration=None, expMonth=None, optionType=None, entitlement=None) -> requests.Response:
526
527
  """
527
528
  Get Option Chain including information on options contracts associated with each expiration for a ticker.
528
529
  :param symbol: ticker symbol
@@ -542,9 +543,9 @@ class Client:
542
543
  :param range: range ("ITM"|"NTM"|"OTM"...)
543
544
  :type range: str
544
545
  :param fromDate: from date
545
- :type fromDate: datetime | str
546
+ :type fromDate: datetime.pyi | str
546
547
  :param toDate: to date
547
- :type toDate: datetime | str
548
+ :type toDate: datetime.pyi | str
548
549
  :param volatility: volatility
549
550
  :type volatility: float
550
551
  :param underlyingPrice: underlying price
@@ -573,7 +574,7 @@ class Client:
573
574
  'expMonth': expMonth, 'optionType': optionType, 'entitlement': entitlement}),
574
575
  timeout=self.timeout)
575
576
 
576
- def option_expiration_chain(self, symbol):
577
+ def option_expiration_chain(self, symbol: str) -> requests.Response:
577
578
  """
578
579
  Get an option expiration chain for a ticker
579
580
  :param symbol: ticker symbol
@@ -586,8 +587,8 @@ class Client:
586
587
  params=self._params_parser({'symbol': symbol}),
587
588
  timeout=self.timeout)
588
589
 
589
- def price_history(self, symbol, periodType=None, period=None, frequencyType=None, frequency=None, startDate=None,
590
- endDate=None, needExtendedHoursData=None, needPreviousClose=None):
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:
591
592
  """
592
593
  Get price history for a ticker
593
594
  :param symbol: ticker symbol
@@ -601,9 +602,9 @@ class Client:
601
602
  :param frequency: frequency (1|5|10|15|30)
602
603
  :type frequency: int
603
604
  :param startDate: start date
604
- :type startDate: datetime | str
605
+ :type startDate: datetime.pyi | str
605
606
  :param endDate: end date
606
- :type endDate: datetime | str
607
+ :type endDate: datetime.pyi | str
607
608
  :param needExtendedHoursData: need extended hours data (True|False)
608
609
  :type needExtendedHoursData: boolean
609
610
  :param needPreviousClose: need previous close (True|False)
@@ -621,7 +622,7 @@ class Client:
621
622
  'needPreviousClose': needPreviousClose}),
622
623
  timeout=self.timeout)
623
624
 
624
- def movers(self, symbol, sort=None, frequency=None):
625
+ def movers(self, symbol: str, sort=None, frequency=None) -> requests.Response:
625
626
  """
626
627
  Get movers in a specific index and direction
627
628
  :param symbol: symbol ("$DJI"|"$COMPX"|"$SPX"|"NYSE"|"NASDAQ"|"OTCBB"|"INDEX_ALL"|"EQUITY_ALL"|"OPTION_ALL"|"OPTION_PUT"|"OPTION_CALL")
@@ -634,17 +635,17 @@ class Client:
634
635
  :rtype: request.Response
635
636
  """
636
637
  return requests.get(f'{self._base_api_url}/marketdata/v1/movers/{symbol}',
637
- headers={'Authorization': f'Bearer {self.access_token}'},
638
+ headers={"accept": "application/json", 'Authorization': f'Bearer {self.access_token}'},
638
639
  params=self._params_parser({'sort': sort, 'frequency': frequency}),
639
640
  timeout=self.timeout)
640
641
 
641
- def market_hours(self, symbols, date=None):
642
+ def market_hours(self, symbols, date=None) -> requests.Response:
642
643
  """
643
644
  Get Market Hours for dates in the future across different markets.
644
645
  :param symbols: list of market symbols ("equity", "option", "bond", "future", "forex")
645
646
  :type symbols: list
646
647
  :param date: date
647
- :type date: datetime | str
648
+ :type date: datetime.pyi | str
648
649
  :return: market hours
649
650
  :rtype: request.Response
650
651
  """
@@ -655,13 +656,13 @@ class Client:
655
656
  'date': self._time_convert(date, 'YYYY-MM-DD')}),
656
657
  timeout=self.timeout)
657
658
 
658
- def market_hour(self, market_id, date=None):
659
+ def market_hour(self, market_id: str, date=None) -> requests.Response:
659
660
  """
660
661
  Get Market Hours for dates in the future for a single market.
661
662
  :param market_id: market id ("equity"|"option"|"bond"|"future"|"forex")
662
663
  :type market_id: str
663
664
  :param date: date
664
- :type date: datetime | str
665
+ :type date: datetime.pyi | str
665
666
  :return: market hours
666
667
  :rtype: request.Response
667
668
  """
@@ -670,7 +671,7 @@ class Client:
670
671
  params=self._params_parser({'date': self._time_convert(date, 'YYYY-MM-DD')}),
671
672
  timeout=self.timeout)
672
673
 
673
- def instruments(self, symbol, projection):
674
+ def instruments(self, symbol: str, projection) -> requests.Response:
674
675
  """
675
676
  Get instruments for a list of symbols
676
677
  :param symbol: symbol
@@ -685,11 +686,11 @@ class Client:
685
686
  params={'symbol': symbol, 'projection': projection},
686
687
  timeout=self.timeout)
687
688
 
688
- def instrument_cusip(self, cusip_id):
689
+ def instrument_cusip(self, cusip_id: str | int) -> requests.Response:
689
690
  """
690
691
  Get instrument for a single cusip
691
692
  :param cusip_id: cusip id
692
- :type cusip_id: str
693
+ :type cusip_id: str|int
693
694
  :return: instrument
694
695
  :rtype: request.Response
695
696
  """
@@ -52,13 +52,18 @@ class Stream:
52
52
  print("Could not get streamerInfo")
53
53
 
54
54
  # start the stream
55
+ start_time = datetime.now()
55
56
  while True:
56
57
  try:
58
+ start_time = datetime.now()
59
+ if self.verbose: print("Connecting to streaming server...")
57
60
  async with websockets.connect(self._streamer_info.get('streamerSocketUrl'), ping_interval=None) as self._websocket:
58
- if self.verbose: print("Connected to streaming server.")
59
-
60
61
  # send login payload
61
- 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")})
62
+ login_payload = self.basic_request(service="ADMIN",
63
+ command="LOGIN",
64
+ parameters={"Authorization": self._client.access_token,
65
+ "SchwabClientChannel": self._streamer_info.get("schwabClientChannel"),
66
+ "SchwabClientFunctionId": self._streamer_info.get("schwabClientFunctionId")})
62
67
  await self._websocket.send(json.dumps(login_payload))
63
68
  receiver_func(await self._websocket.recv(), *args, **kwargs)
64
69
  self.active = True
@@ -67,7 +72,10 @@ class Stream:
67
72
  for service, subs in self.subscriptions.items():
68
73
  reqs = []
69
74
  for key, fields in subs.items():
70
- reqs.append(self.basic_request(service=service, command="ADD", parameters={"keys": key, "fields": Stream._list_to_string(fields)}))
75
+ reqs.append(self.basic_request(service=service,
76
+ command="ADD",
77
+ parameters={"keys": key,
78
+ "fields": Stream._list_to_string(fields)}))
71
79
  if reqs:
72
80
  await self._websocket.send(json.dumps({"requests": reqs}))
73
81
  receiver_func(await self._websocket.recv(), *args, **kwargs)
@@ -84,6 +92,9 @@ class Stream:
84
92
  elif e is websockets.exceptions.ConnectionClosedError or str(e) == "no close frame received or sent": # catch no subscriptions kick
85
93
  if self.verbose: print(f"Stream closed (likely no subscriptions): {e}")
86
94
  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.")
97
+ break
87
98
  else: # stream has quit unexpectedly, try to reconnect
88
99
  if self.verbose: print(f"{e}")
89
100
  if self.verbose: print("Connection lost to server, reconnecting...")
@@ -100,7 +111,7 @@ class Stream:
100
111
 
101
112
  self._thread = threading.Thread(target=_start_async, daemon=False)
102
113
  self._thread.start()
103
- sleep(1) # if the thread does not start in time then the main program may close before the streamer starts
114
+ # if the thread does not start in time then the main program may close before the streamer starts
104
115
  else:
105
116
  print("Stream already active.")
106
117
 
@@ -126,6 +137,8 @@ class Stream:
126
137
  while True:
127
138
  in_hours = (start <= datetime.now().time() <= end) and (0 <= datetime.now().weekday() <= 4)
128
139
  if in_hours and not self.active:
140
+ if len(self.subscriptions) == 0:
141
+ if self.verbose: print("No subscriptions, starting stream anyways.")
129
142
  self.start(receiver=receiver)
130
143
  elif not in_hours and self.active:
131
144
  if self.verbose: print("Stopping Stream.")
@@ -260,7 +273,7 @@ class Stream:
260
273
  if type(ls) is str: return ls
261
274
  elif type(ls) is list: return ",".join(map(str, ls))
262
275
 
263
- def level_one_equities(self, keys, fields, command="ADD"):
276
+ def level_one_equities(self, keys: str | list, fields: str | list, command="ADD") -> dict:
264
277
  """
265
278
  Level one equities
266
279
  :param keys: list of keys to use (e.g. ["AMD", "INTC"])
@@ -274,7 +287,7 @@ class Stream:
274
287
  """
275
288
  return self.basic_request("LEVELONE_EQUITIES", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
276
289
 
277
- def level_one_options(self, keys, fields, command="ADD"):
290
+ def level_one_options(self, keys: str | list, fields: str | list, command="ADD") -> dict:
278
291
  """
279
292
  Level one options, key format: [Underlying Symbol (6 characters including spaces) | Expiration (6 characters) | Call/Put (1 character) | Strike Price (5+3=8 characters)]
280
293
  :param keys: list of keys to use (e.g. ["GOOG 240809C00095000", "AAPL 240517P00190000"])
@@ -288,7 +301,7 @@ class Stream:
288
301
  """
289
302
  return self.basic_request("LEVELONE_OPTIONS", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
290
303
 
291
- def level_one_futures(self, keys, fields, command="ADD"):
304
+ def level_one_futures(self, keys: str | list, fields: str | list, command="ADD") -> dict:
292
305
  """
293
306
  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)
294
307
  :param keys: list of keys to use (e.g. ["/ESF24", "/GCG24"])
@@ -302,7 +315,7 @@ class Stream:
302
315
  """
303
316
  return self.basic_request("LEVELONE_FUTURES", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
304
317
 
305
- def level_one_futures_options(self, keys, fields, command="ADD"):
318
+ def level_one_futures_options(self, keys: str | list, fields: str | list, command="ADD") -> dict:
306
319
  """
307
320
  Level one futures options, key format: '.' + '/' + 'root symbol' + 'month code' + 'year code' + 'Call/Put code' + 'Strike Price'
308
321
  :param keys: list of keys to use (e.g. ["./OZCZ23C565"])
@@ -316,7 +329,7 @@ class Stream:
316
329
  """
317
330
  return self.basic_request("LEVELONE_FUTURES_OPTIONS", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
318
331
 
319
- def level_one_forex(self, keys, fields, command="ADD"):
332
+ def level_one_forex(self, keys: str | list, fields: str | list, command="ADD") -> dict:
320
333
  """
321
334
  Level one forex, key format: 'from currency' + '/' + 'to currency'
322
335
  :param keys: list of keys to use (e.g. ["EUR/USD", "JPY/USD"])
@@ -330,7 +343,7 @@ class Stream:
330
343
  """
331
344
  return self.basic_request("LEVELONE_FOREX", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
332
345
 
333
- def nyse_book(self, keys, fields, command="ADD"):
346
+ def nyse_book(self, keys: str | list, fields: str | list, command="ADD") -> dict:
334
347
  """
335
348
  NYSE book orders
336
349
  :param keys: list of keys to use (e.g. ["NIO", "F"])
@@ -344,7 +357,7 @@ class Stream:
344
357
  """
345
358
  return self.basic_request("NYSE_BOOK", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
346
359
 
347
- def nasdaq_book(self, keys, fields, command="ADD"):
360
+ def nasdaq_book(self, keys: str | list, fields: str | list, command="ADD") -> dict:
348
361
  """
349
362
  NASDAQ book orders
350
363
  :param keys: list of keys to use (e.g. ["AMD", "CRWD"])
@@ -358,7 +371,7 @@ class Stream:
358
371
  """
359
372
  return self.basic_request("NASDAQ_BOOK", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
360
373
 
361
- def options_book(self, keys, fields, command="ADD"):
374
+ def options_book(self, keys: str | list, fields: str | list, command="ADD") -> dict:
362
375
  """
363
376
  Options book orders
364
377
  :param keys: list of keys to use (e.g. ["GOOG 240809C00095000", "AAPL 240517P00190000"])
@@ -372,7 +385,7 @@ class Stream:
372
385
  """
373
386
  return self.basic_request("OPTIONS_BOOK", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
374
387
 
375
- def chart_equity(self, keys, fields, command="ADD"):
388
+ def chart_equity(self, keys: str | list, fields: str | list, command="ADD") -> dict:
376
389
  """
377
390
  Chart equity
378
391
  :param keys: list of keys to use (e.g. ["GOOG", "AAPL"])
@@ -386,7 +399,7 @@ class Stream:
386
399
  """
387
400
  return self.basic_request("CHART_EQUITY", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
388
401
 
389
- def chart_futures(self, keys, fields, command="ADD"):
402
+ def chart_futures(self, keys: str | list, fields: str | list, command="ADD") -> dict:
390
403
  """
391
404
  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)
392
405
  :param keys: list of keys to use (e.g. ["/ESF24", "/GCG24"])
@@ -400,7 +413,7 @@ class Stream:
400
413
  """
401
414
  return self.basic_request("CHART_FUTURES", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
402
415
 
403
- def screener_equity(self, keys, fields, command="ADD"):
416
+ def screener_equity(self, keys: str | list, fields: str | list, command="ADD") -> dict:
404
417
  """
405
418
  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)
406
419
  :param keys: list of keys to use (e.g. ["$DJI_PERCENT_CHANGE_UP_60", "NASDAQ_VOLUME_30"])
@@ -414,7 +427,7 @@ class Stream:
414
427
  """
415
428
  return self.basic_request("SCREENER_EQUITY", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
416
429
 
417
- def screener_option(self, keys, fields, command="ADD"):
430
+ def screener_options(self, keys: str | list, fields: str | list, command="ADD") -> dict:
418
431
  """
419
432
  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)
420
433
  :param keys: list of keys to use (e.g. ["OPTION_PUT_PERCENT_CHANGE_UP_60", "OPTION_CALL_TRADES_30"])
@@ -428,7 +441,7 @@ class Stream:
428
441
  """
429
442
  return self.basic_request("SCREENER_OPTION", command, parameters={"keys": Stream._list_to_string(keys), "fields": Stream._list_to_string(fields)})
430
443
 
431
- def account_activity(self, keys="Account Activity", fields="0,1,2,3", command="SUBS"):
444
+ def account_activity(self, keys="Account Activity", fields="0,1,2,3", command="SUBS") -> dict:
432
445
  """
433
446
  Account activity
434
447
  :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.0
3
+ Version: 2.2.2
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
@@ -19,6 +19,8 @@ Classifier: Natural Language :: English
19
19
  Requires-Python: >=3.11
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE.txt
22
+ Requires-Dist: requests
23
+ Requires-Dist: websockets
22
24
 
23
25
  # Schwab-API-Python
24
26
  ![PyPI - Version](https://img.shields.io/pypi/v/schwabdev) ![Discord](https://img.shields.io/discord/1076596998150561873?logo=discord) ![PyPI - Downloads](https://img.shields.io/pypi/dm/schwabdev) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate/?business=8VDFKHMBFSC2Q&no_recurring=0&currency_code=USD) ![YouTube Video Views](https://img.shields.io/youtube/views/kHbom0KIJwc?style=flat&logo=youtube)
@@ -54,6 +56,7 @@ print(client.account_linked().json()) #make api calls
54
56
  - Functions for all api functions (examples in `examples/api_demo.py`)
55
57
  - Stream real-time data with a customizable response handler (examples in `examples/stream_demo.py`)
56
58
  ### TBD
59
+ - Paper trading client
57
60
  - Automatic refresh token updates. (Waiting for Schwab implementation)
58
61
  ### Notes
59
62
  The schwabdev folder contains code for main operations:
@@ -3,7 +3,6 @@ README.md
3
3
  setup.py
4
4
  schwabdev/__init__.py
5
5
  schwabdev/api.py
6
- schwabdev/color_print.py
7
6
  schwabdev/stream.py
8
7
  schwabdev.egg-info/PKG-INFO
9
8
  schwabdev.egg-info/SOURCES.txt
@@ -1,6 +1,6 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
- VERSION = '2.2.0'
3
+ VERSION = '2.2.2'
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()
@@ -1,19 +0,0 @@
1
- """
2
- This file is used to print colored text
3
- Github: https://github.com/tylerebowers/Schwab-API-Python
4
- """
5
-
6
-
7
- def info(string, end="\n"): print(f"\033[92m{'[INFO]: '}\033[00m{string}", end=end)
8
-
9
-
10
- def warning(string, end="\n"): print(f"\033[93m{'[WARN]: '}\033[00m{string}", end=end)
11
-
12
-
13
- def error(string, end="\n"): print(f"\033[91m{'[ERROR]: '}\033[00m{string}", end=end)
14
-
15
-
16
- def user(string, end="\n"): print(f"\033[94m{'[USER]: '}\033[00m{string}", end=end)
17
-
18
-
19
- def user_input(string): return input(f"\033[94m{'[INPUT]: '}\033[00m{string}")
File without changes
File without changes