Skip to content

Commit c361578

Browse files
committed
changes from mosting everything works on main
1 parent d6459d4 commit c361578

File tree

4 files changed

+108
-16
lines changed

4 files changed

+108
-16
lines changed

pythonic_schwab_api/algo_example_script.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ def actually_do_some_trading(orders_api, account_hash, valid_quotes):
5151

5252

5353
def find_trades_from_quotes(orders_api, quotes, account_hash):
54+
if not quotes or len(quotes) == 0:
55+
print("No quotes found.")
56+
return
5457
# Convert quotes to DataFrame
5558
quotes_df = pd.DataFrame.from_dict(quotes, orient='index')
5659
quotes_df.index = quotes_df.index.map(urll.unquote)
@@ -61,6 +64,9 @@ def find_trades_from_quotes(orders_api, quotes, account_hash):
6164
# Filter out quotes with missing data
6265
quotes_df = quotes_df.dropna(subset=['askPrice', 'askSize', 'bidPrice', 'bidSize', 'lastPrice'])
6366

67+
if 'regular' not in quotes_df.columns:
68+
return
69+
6470
# Apply trading logic
6571
valid_quotes = quotes_df[
6672
(quotes_df['lastPrice'] >= 0.005) &
@@ -116,8 +122,8 @@ def sell_the_algo_buys(orders_api, account_hash, quotes_api):
116122

117123
def check_cash_account(account_api, account_hash):
118124
account = account_api.get_account(account_hash=account_hash, fields="positions")
119-
print(account)
120-
cash_account = account['cash']
125+
# print(account)
126+
cash_account = account["securitiesAccount"]["type"] == "CASH"
121127
return cash_account
122128

123129

@@ -147,4 +153,4 @@ def main():
147153
find_trades_from_quotes(orders_api, quotes, account_hash)
148154

149155
# Sell trades
150-
sell_the_algo_buys(orders_api, account_hash, quotes_api)
156+
sell_the_algo_buys(orders_api, account_hash, quotes_api)

pythonic_schwab_api/api_client.py

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import random
2+
import re
23
from time import sleep
34
import requests
45
import webbrowser
@@ -85,7 +86,7 @@ def refresh_access_token(self):
8586
}
8687
if not self.post_token_request(data):
8788
self.logger.error("Failed to refresh access token.")
88-
return False
89+
self.manual_authorization_flow()
8990
self.token_info = self.load_token()
9091
return self.validate_token()
9192

@@ -119,7 +120,8 @@ def validate_token(self, force=False):
119120
print("Token expired or invalid.")
120121
# get AAPL to validate token
121122
params = {'symbol': 'AAPL'}
122-
response = self.make_request(endpoint=f"{self.config.MARKET_DATA_BASE_URL}/chains", params=params, validating=True)
123+
response = self.make_request(endpoint=f"{self.config.MARKET_DATA_BASE_URL}/chains", params=params,
124+
validating=True)
123125
print(response)
124126
if response:
125127
self.logger.info("Token validated successfully.")
@@ -129,34 +131,81 @@ def validate_token(self, force=False):
129131
return False
130132

131133
def make_request(self, endpoint, method="GET", **kwargs):
134+
"""
135+
Make authenticated HTTP requests.
136+
137+
Args:
138+
endpoint (str): The API endpoint.
139+
method (str, optional): The HTTP method. Defaults to "GET".
140+
**kwargs: Additional parameters for the request.
141+
142+
Returns:
143+
dict: The JSON response from the API if available, else None.
144+
145+
Raises:
146+
HTTPError: If the request fails.
147+
"""
148+
# Introduce a random delay to avoid hitting the server too quickly
132149
sleep(0.5 + random.randint(0, 1000) / 1000)
133-
""" Make authenticated HTTP requests. """
150+
151+
# Validate the token if not in a validation process
134152
if 'validating' not in kwargs:
135153
if not self.validate_token():
136154
self.logger.info("Token expired or invalid, re-authenticating.")
137-
self.manual_authorization_flow()
155+
self.refresh_access_token()
138156
kwargs.pop('validating', None)
157+
158+
# Construct the full URL if the base URL is not included
139159
if self.config.API_BASE_URL not in endpoint:
140160
url = f"{self.config.API_BASE_URL}{endpoint}"
141161
else:
142162
url = endpoint
143-
print(f"Making request to {url} with method {method} and kwargs {kwargs} (validating already popped if present)")
163+
164+
self.logger.debug(f"Making request to {url} with method {method} and kwargs {kwargs}")
165+
166+
# Set the authorization headers
144167
headers = {'Authorization': f"Bearer {self.token_info['access_token']}"}
168+
169+
# Make the HTTP request
145170
response = self.session.request(method, url, headers=headers, **kwargs)
146-
print(response.status_code)
147-
print(response.text)
171+
172+
# Handle token expiration during the request
148173
if response.status_code == 401:
149174
self.logger.warning("Token expired during request. Refreshing token...")
150-
self.manual_authorization_flow()
175+
self.refresh_access_token()
151176
headers = {'Authorization': f"Bearer {self.token_info['access_token']}"}
152177
response = self.session.request(method, url, headers=headers, **kwargs)
178+
179+
# Log for non-200 responses
180+
if response.status_code != 200:
181+
order_pattern = r"https://api\.schwabapi\.com/trader/v1/accounts/.*/orders"
182+
if response.status_code == 201 and re.match(order_pattern, url):
183+
location = response.headers.get('location')
184+
if location:
185+
order_id = location.split('/')[-1]
186+
self.logger.debug(f"Order placed successfully. Order ID: {order_id}")
187+
return {"order_id": order_id, "success": True}
188+
else:
189+
self.logger.error("201 response without a location header.")
190+
return None
191+
153192
response.raise_for_status()
154-
return response.json()
193+
194+
# Check if response content is empty before parsing as JSON
195+
if response.content:
196+
try:
197+
return response.json()
198+
except json.JSONDecodeError as e:
199+
self.logger.error(f"Error decoding JSON response: {e}")
200+
raise requests.exceptions.JSONDecodeError(e.msg, e.doc, e.pos)
201+
else:
202+
self.logger.debug("Empty response content")
203+
return None
155204

156205
def get_user_preferences(self):
157206
"""Retrieve user preferences."""
158207
try:
159208
return self.make_request(f'{self.config.TRADER_BASE_URL}/userPreference')
160209
except Exception as e:
161210
self.logger.error(f"Failed to get user preferences: {e}")
162-
return None
211+
return None

pythonic_schwab_api/market_data.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import urllib.parse as urll
2+
3+
14
class Quotes:
25
def __init__(self, client):
36
self.client = client
@@ -13,6 +16,8 @@ def get_list(self, symbols=None, fields=None, indicative=False):
1316

1417
def get_single(self, symbol_id, fields=None):
1518
params = {'fields': fields}
19+
if urll.unquote(symbol_id) == symbol_id:
20+
symbol_id = urll.quote(symbol_id)
1621
return self.client.make_request(f"{self.base_url}/{symbol_id}/quotes", params=params)
1722

1823

@@ -70,4 +75,4 @@ def by_symbol(self, symbol, projection):
7075
return self.client.make_request(self.base_url, params=params)
7176

7277
def by_cusip(self, cusip_id):
73-
return self.client.make_request(f"{self.base_url}/{cusip_id}")
78+
return self.client.make_request(f"{self.base_url}/{cusip_id}")

pythonic_schwab_api/orders.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@ def __init__(self, client):
66
self.client = client
77
self.base_url = client.config.ORDERS_BASE_URL
88

9+
def create_order_schema(self, symbol, side, quantity, order_type='MARKET', limit_price=None, time_in_force='DAY', session='NORMAL'):
10+
"""Create a new order schema."""
11+
if order_type not in ['MARKET']:
12+
quantity = int(quantity)
13+
order = {
14+
'orderType': order_type,
15+
'session': session,
16+
'duration': time_in_force,
17+
'orderStrategyType': 'SINGLE',
18+
'orderLegCollection': [
19+
{
20+
'instruction': side,
21+
'quantity': quantity,
22+
'instrument': {
23+
'symbol': symbol,
24+
'assetType': 'EQUITY'
25+
}
26+
}
27+
]
28+
}
29+
if order_type == 'LIMIT':
30+
order['price'] = limit_price
31+
return order
32+
933
def get_orders(self, account_hash, max_results=100, from_entered_time=None, to_entered_time=None, status=None):
1034
"""Retrieve a list of orders for a specified account."""
1135
if from_entered_time is None:
@@ -21,10 +45,18 @@ def get_orders(self, account_hash, max_results=100, from_entered_time=None, to_e
2145
endpoint = f"{self.base_url}/{account_hash}/orders"
2246
return self.client.make_request(endpoint, params=params)
2347

48+
def preview_order(self, account_hash, order_details):
49+
"""Preview a new order for an account."""
50+
# TODO: Coming Soon per the API documentation
51+
return "test"
52+
endpoint = f"{self.base_url}/{account_hash}/orders/previewOrder"
53+
return self.client.make_request(method="POST", endpoint=endpoint, data=order_details)
54+
2455
def place_order(self, account_hash, order_details):
2556
"""Place a new order for an account."""
2657
endpoint = f"{self.base_url}/{account_hash}/orders"
27-
return self.client.post(endpoint, data=order_details)
58+
filtered_order_details = {k: v for k, v in order_details.items() if v is not None}
59+
return self.client.make_request(method="POST", endpoint=endpoint, json=filtered_order_details)
2860

2961
def get_order(self, account_hash, order_id):
3062
"""Retrieve details for a specific order."""
@@ -39,4 +71,4 @@ def cancel_order(self, account_hash, order_id):
3971
def replace_order(self, account_hash, order_id, new_order_details):
4072
"""Replace an existing order with new details."""
4173
endpoint = f"{self.base_url}/{account_hash}/orders/{order_id}"
42-
return self.client.make_request(endpoint, method='PUT', data=new_order_details)
74+
return self.client.make_request(endpoint, method='PUT', data=new_order_details)

0 commit comments

Comments
 (0)