Skip to content

Commit 240e608

Browse files
author
Rob Smith
committed
all the files
1 parent 88c7691 commit 240e608

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2818
-0
lines changed

build/lib/pythonic_schwab_api/__init__.py

Whitespace-only changes.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import datetime
2+
import logging
3+
4+
5+
class Accounts:
6+
def __init__(self, client):
7+
self.client = client
8+
self.logger = logging.getLogger(__name__)
9+
self.base_url = client.config.ACCOUNTS_BASE_URL
10+
11+
def get_account_numbers(self):
12+
"""Retrieve account numbers associated with the user's profile."""
13+
try:
14+
print(f'{self.base_url}/accountNumbers')
15+
return self.client.make_request(f'{self.base_url}/accountNumbers')
16+
except Exception as e:
17+
self.logger.error(f"Failed to get account numbers: {e}")
18+
return None
19+
20+
def get_all_accounts(self, fields=None):
21+
"""Retrieve detailed information for all linked accounts, optionally filtering the fields."""
22+
params = {'fields': fields} if fields else {}
23+
try:
24+
return self.client.make_request(f'{self.base_url}', params=params)
25+
except Exception as e:
26+
self.logger.error(f"Failed to get all accounts: {e}")
27+
return None
28+
29+
def get_account(self, account_hash, fields=None):
30+
"""Retrieve detailed information for a specific account using its hash."""
31+
if not account_hash:
32+
self.logger.error("Account hash is required for getting account details")
33+
return None
34+
params = {'fields': fields} if fields else {}
35+
try:
36+
return self.client.make_request(f'{self.base_url}/{account_hash}', params=params)
37+
except Exception as e:
38+
self.logger.error(f"Failed to get account {account_hash}: {e}")
39+
return None
40+
41+
def get_account_transactions(self, account_hash, start_date, end_date, types=None, symbol=None):
42+
"""Retrieve transactions for a specific account over a specified date range."""
43+
if not (isinstance(start_date, datetime.datetime) and isinstance(end_date, datetime.datetime)):
44+
self.logger.error("Invalid date format. Dates must be datetime objects")
45+
return None
46+
params = {
47+
'startDate': start_date.isoformat(),
48+
'endDate': end_date.isoformat(),
49+
'types': types,
50+
'symbol': symbol
51+
}
52+
try:
53+
return self.client.make_request(f'{self.base_url}/{account_hash}/transactions', params=params)
54+
except Exception as e:
55+
self.logger.error(f"Failed to get transactions for account {account_hash}: {e}")
56+
return None
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""
2+
This is an example of how to use various functions/methods/classes in the repo.
3+
This is beyond not financial advice.
4+
This is for demonstration purposes only.
5+
Please do not ping me if you hook this chat-gpt-looking code up to a real brokerage account and just letterrip.
6+
"""
7+
8+
import json
9+
from datetime import datetime, timedelta
10+
import pandas as pd
11+
import urllib.parse as urll
12+
from pythonic_schwab_api.accounts import Accounts
13+
from pythonic_schwab_api.api_client import APIClient
14+
from orders import Orders
15+
from market_data import Quotes
16+
from tqdm import tqdm
17+
18+
19+
def actually_do_some_trading(orders_api, account_hash, valid_quotes):
20+
traded_tickers = []
21+
# Process valid trades
22+
for ticker, row in tqdm(valid_quotes.iterrows(), total=valid_quotes.shape[0], desc="Processing trades"):
23+
bid_price = row['bidPrice']
24+
ask_price = row['askPrice']
25+
last_price = row['lastPrice']
26+
print(f"{ticker}")
27+
print(f"Bid price: {bid_price} | Ask price: {ask_price} | Last price: {last_price}")
28+
29+
try:
30+
response = orders_api.place_order(account_hash=account_hash,
31+
symbol=ticker,
32+
side='BUY',
33+
quantity=6,
34+
order_type='LIMIT',
35+
limit_price=bid_price + 0.01,
36+
time_in_force='DAY')
37+
print(f"Order response: {response}")
38+
# write order number, ticker to algo_trades.json
39+
try:
40+
with open(f"algo_trades_{orders_api.client.config.initials}.json", "r") as f:
41+
data = json.load(f)
42+
except FileNotFoundError:
43+
data = []
44+
data.append({"ticker": ticker, "order_number": response['order_number']})
45+
with open(f"algo_trades_{orders_api.client.config.initials}.json", "w") as f:
46+
json.dump(data, f)
47+
traded_tickers.append(ticker)
48+
except Exception as e:
49+
print(f"Error placing order: {e}")
50+
return traded_tickers
51+
52+
53+
def find_trades_from_quotes(orders_api, quotes, account_hash):
54+
# Convert quotes to DataFrame
55+
quotes_df = pd.DataFrame.from_dict(quotes, orient='index')
56+
quotes_df.index = quotes_df.index.map(urll.unquote)
57+
58+
# Extract relevant quote data
59+
quotes_df = quotes_df['quote'].apply(pd.Series)
60+
61+
# Filter out quotes with missing data
62+
quotes_df = quotes_df.dropna(subset=['askPrice', 'askSize', 'bidPrice', 'bidSize', 'lastPrice'])
63+
64+
# Apply trading logic
65+
valid_quotes = quotes_df[
66+
(quotes_df['lastPrice'] >= 0.005) &
67+
(quotes_df['lastPrice'] <= 0.515) &
68+
((quotes_df['askPrice'] - quotes_df['bidPrice']) >= 0.06) &
69+
(quotes_df['regular']['regularMarketLastPrice'] / (quotes_df['askPrice'] - quotes_df['bidPrice']) <= 10) &
70+
(quotes_df['askSize'] <= 100) &
71+
(quotes_df['bidSize'] <= 100) &
72+
((quotes_df['lastPrice'] - quotes_df['bidPrice']) >= 0) &
73+
((quotes_df['lastPrice'] - quotes_df['askPrice']) >= 0) &
74+
((quotes_df['lastPrice'] - quotes_df['bidPrice']) <= 0.7 * (quotes_df['askPrice'] - quotes_df['bidPrice'])) &
75+
((quotes_df['lastPrice'] - quotes_df['askPrice']) <= 0.7 * (quotes_df['askPrice'] - quotes_df['bidPrice']))
76+
]
77+
return valid_quotes
78+
79+
80+
def sell_the_algo_buys(orders_api, account_hash, quotes_api):
81+
# Check order statuses and handle new trades
82+
try:
83+
with open(f"algo_trades_{orders_api.client.config.initials}.json", "r") as f:
84+
data = json.load(f)
85+
except FileNotFoundError:
86+
data = []
87+
data_tickers = [trade['ticker'] for trade in data]
88+
data_quotes = quotes_api.get_list(data_tickers)
89+
all_orders = orders_api.get_orders(account_hash=account_hash,
90+
maxResults=3000,
91+
fromEnteredTime=(datetime.now() - timedelta(days=360)).isoformat(timespec='seconds'),
92+
toEnteredTime=datetime.now().isoformat(timespec='seconds'))
93+
for trade in data:
94+
try:
95+
response = all_orders.get(trade['order_number'])
96+
print(f"Order status for {trade['ticker']}: {response['status']}")
97+
bought_price = response['price']
98+
if response['status'] in ['FILLED', 'PARTIAL']:
99+
new_quote = data_quotes.get(trade['ticker'])
100+
new_quote_data = new_quote.get("quote")
101+
if new_quote_data:
102+
ask_price = new_quote_data.get('askPrice')
103+
if ask_price and bought_price <= ask_price - 0.03:
104+
response = orders_api.place_order(account_hash=account_hash,
105+
symbol=trade['ticker'],
106+
side='SELL',
107+
quantity=1,
108+
order_type='LIMIT',
109+
limit_price=ask_price - 0.01,
110+
time_in_force='DAY')
111+
print(f"Order response: {response}")
112+
print(f"Order response: {response}")
113+
except Exception as e:
114+
print(f"Error checking order status: {e}")
115+
116+
117+
def check_cash_account(account_api, account_hash):
118+
account = account_api.get_account(account_hash=account_hash, fields="positions")
119+
print(account)
120+
cash_account = account['cash']
121+
return cash_account
122+
123+
124+
def main():
125+
# Initialize client
126+
client = APIClient(initials="your_initials_here")
127+
accounts_api = Accounts(client)
128+
orders_api = Orders(client)
129+
quotes_api = Quotes(client)
130+
131+
# Get account hash
132+
sample_account = client.account_numbers[0]
133+
account_hash = sample_account['hashValue']
134+
135+
# Check cash account
136+
if not check_cash_account(accounts_api, account_hash):
137+
print("Confirm pattern day trader status or using a cash account.")
138+
return
139+
140+
# Get quotes
141+
quotes = quotes_api.get_list(["AAPL", "AMD", "TSLA", "MSFT", "GOOGL", "AMZN", "NFLX", "NVDA", "INTC", "CSCO"])
142+
"""
143+
If you are wondering how to get a list of tickers that meet some given criteria, check out the get_finviz repo
144+
"""
145+
146+
# Find trades
147+
find_trades_from_quotes(orders_api, quotes, account_hash)
148+
149+
# Sell trades
150+
sell_the_algo_buys(orders_api, account_hash, quotes_api)
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import random
2+
from time import sleep
3+
import requests
4+
import webbrowser
5+
import base64
6+
import json
7+
from datetime import datetime, timedelta
8+
import logging
9+
from config import APIConfig
10+
from color_print import ColorPrint
11+
12+
13+
class APIClient:
14+
def __init__(self, initials):
15+
self.initials = initials
16+
self.account_numbers = None
17+
self.config = APIConfig(self.initials)
18+
self.session = requests.Session()
19+
self.setup_logging()
20+
self.token_info = self.load_token()
21+
22+
# Validate and refresh token or reauthorize if necessary
23+
if not self.token_info or not self.ensure_valid_token():
24+
self.manual_authorization_flow()
25+
26+
def setup_logging(self):
27+
logging.basicConfig(**self.config.LOGGING_CONFIG)
28+
self.logger = logging.getLogger(__name__)
29+
30+
def ensure_valid_token(self):
31+
"""Ensure the token is valid, refresh if possible, otherwise prompt for reauthorization."""
32+
if self.token_info:
33+
if self.validate_token():
34+
self.logger.info("Token loaded and valid.")
35+
return True
36+
elif 'refresh_token' in self.token_info:
37+
self.logger.info("Access token expired. Attempting to refresh.")
38+
if self.refresh_access_token():
39+
return True
40+
self.logger.warning("Token invalid and could not be refreshed.")
41+
return False
42+
43+
def manual_authorization_flow(self):
44+
""" Handle the manual steps required to get the authorization code from the user. """
45+
self.logger.info("Starting manual authorization flow.")
46+
auth_url = f"{self.config.API_BASE_URL}/v1/oauth/authorize?client_id={self.config.APP_KEY}&redirect_uri={self.config.CALLBACK_URL}&response_type=code"
47+
webbrowser.open(auth_url)
48+
self.logger.info(f"Please authorize the application by visiting: {auth_url}")
49+
response_url = ColorPrint.input(
50+
"After authorizing, wait for it to load (<1min) and paste the WHOLE url here: ")
51+
authorization_code = f"{response_url[response_url.index('code=') + 5:response_url.index('%40')]}@"
52+
# session = response_url[response_url.index("session=")+8:]
53+
self.exchange_authorization_code_for_tokens(authorization_code)
54+
55+
def exchange_authorization_code_for_tokens(self, code):
56+
""" Exchange the authorization code for access and refresh tokens. """
57+
data = {
58+
'grant_type': 'authorization_code',
59+
'code': code,
60+
'redirect_uri': self.config.CALLBACK_URL
61+
}
62+
self.post_token_request(data)
63+
64+
def post_token_request(self, data):
65+
""" Generalized token request handling. """
66+
headers = {
67+
'Authorization': f'Basic {base64.b64encode(f"{self.config.APP_KEY}:{self.config.APP_SECRET}".encode()).decode()}',
68+
'Content-Type': 'application/x-www-form-urlencoded'
69+
}
70+
response = self.session.post(f"{self.config.API_BASE_URL}/v1/oauth/token", headers=headers, data=data)
71+
if response.status_code == 200:
72+
self.save_token(response.json())
73+
self.logger.info("Tokens successfully updated.")
74+
return True
75+
else:
76+
self.logger.error("Failed to obtain tokens.")
77+
response.raise_for_status()
78+
79+
def refresh_access_token(self):
80+
"""Use the refresh token to obtain a new access token and validate it."""
81+
82+
data = {
83+
'grant_type': 'refresh_token',
84+
'refresh_token': self.token_info['refresh_token']
85+
}
86+
if not self.post_token_request(data):
87+
self.logger.error("Failed to refresh access token.")
88+
return False
89+
self.token_info = self.load_token()
90+
return self.validate_token()
91+
92+
def save_token(self, token_data):
93+
""" Save token data securely. """
94+
token_data['expires_at'] = (datetime.now() + timedelta(seconds=token_data['expires_in'])).isoformat()
95+
with open(f'schwab_token_data_{self.initials}.json', 'w') as f:
96+
json.dump(token_data, f)
97+
self.logger.info("Token data saved successfully.")
98+
99+
def load_token(self):
100+
""" Load token data. """
101+
try:
102+
with open(f'schwab_token_data_{self.initials}.json', 'r') as f:
103+
token_data = json.load(f)
104+
return token_data
105+
except Exception as e:
106+
self.logger.warning(f"Loading token failed: {e}")
107+
return None
108+
109+
def validate_token(self, force=False):
110+
""" Validate the current token. """
111+
# print(self.token_info['expires_at'])
112+
# print(datetime.now())
113+
# print(datetime.fromisoformat(self.token_info['expires_at']))
114+
# print(datetime.now() < datetime.fromisoformat(self.token_info['expires_at']))
115+
if self.token_info and datetime.now() < datetime.fromisoformat(self.token_info['expires_at']):
116+
print(f"Token expires in {datetime.fromisoformat(self.token_info['expires_at']) - datetime.now()} seconds")
117+
return True
118+
elif force:
119+
print("Token expired or invalid.")
120+
# get AAPL to validate token
121+
params = {'symbol': 'AAPL'}
122+
response = self.make_request(endpoint=f"{self.config.MARKET_DATA_BASE_URL}/chains", params=params, validating=True)
123+
print(response)
124+
if response:
125+
self.logger.info("Token validated successfully.")
126+
# self.account_numbers = response.json()
127+
return True
128+
self.logger.warning("Token validation failed.")
129+
return False
130+
131+
def make_request(self, endpoint, method="GET", **kwargs):
132+
sleep(0.5 + random.randint(0, 1000) / 1000)
133+
""" Make authenticated HTTP requests. """
134+
if 'validating' not in kwargs:
135+
if not self.validate_token():
136+
self.logger.info("Token expired or invalid, re-authenticating.")
137+
self.manual_authorization_flow()
138+
kwargs.pop('validating', None)
139+
if self.config.API_BASE_URL not in endpoint:
140+
url = f"{self.config.API_BASE_URL}{endpoint}"
141+
else:
142+
url = endpoint
143+
print(f"Making request to {url} with method {method} and kwargs {kwargs} (validating already popped if present)")
144+
headers = {'Authorization': f"Bearer {self.token_info['access_token']}"}
145+
response = self.session.request(method, url, headers=headers, **kwargs)
146+
print(response.status_code)
147+
print(response.text)
148+
if response.status_code == 401:
149+
self.logger.warning("Token expired during request. Refreshing token...")
150+
self.manual_authorization_flow()
151+
headers = {'Authorization': f"Bearer {self.token_info['access_token']}"}
152+
response = self.session.request(method, url, headers=headers, **kwargs)
153+
response.raise_for_status()
154+
return response.json()
155+
156+
def get_user_preferences(self):
157+
"""Retrieve user preferences."""
158+
try:
159+
return self.make_request(f'{self.config.TRADER_BASE_URL}/userPreference')
160+
except Exception as e:
161+
self.logger.error(f"Failed to get user preferences: {e}")
162+
return None
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class ColorPrint:
2+
COLORS = {
3+
'info': '\033[92m[INFO]: \033[00m',
4+
'warning': '\033[93m[WARN]: \033[00m',
5+
'error': '\033[91m[ERROR]: \033[00m',
6+
'input': '\033[94m[INPUT]: \033[00m',
7+
'user': '\033[1;31m[USER]: \033[00m'
8+
}
9+
10+
@staticmethod
11+
def print(message_type, message, end="\n"):
12+
print(f"{ColorPrint.COLORS.get(message_type, '[UNKNOWN]: ')}{message}", end=end)
13+
14+
@staticmethod
15+
def input(message):
16+
return input(f"{ColorPrint.COLORS['input']}{message}")
17+
18+
@staticmethod
19+
def user_input(message):
20+
return input(f"{ColorPrint.COLORS['user']}{message}")
21+
22+
23+
24+
if __name__ == '__main__':
25+
ColorPrint.print('info', 'This is an informational message')

0 commit comments

Comments
 (0)