Skip to content

Commit 1b938d3

Browse files
committed
mostly everything works
1 parent 4bb9bac commit 1b938d3

File tree

4 files changed

+104
-12
lines changed

4 files changed

+104
-12
lines changed

algo_example_script.py

Lines changed: 8 additions & 2 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

api_client.py

Lines changed: 58 additions & 9 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,29 +131,76 @@ 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."""

market_data.py

Lines changed: 5 additions & 0 deletions
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

orders.py

Lines changed: 33 additions & 1 deletion
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."""

0 commit comments

Comments
 (0)