Skip to content

Commit d8d3d1b

Browse files
authored
Merge pull request #9 from Patch-Code-Prosperity/sandbox
Sandbox
2 parents 804f273 + 188b22d commit d8d3d1b

File tree

5 files changed

+85
-48
lines changed

5 files changed

+85
-48
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
.idea/
22
__pycache__/
33
.env
4-
token_data.json
4+
token_data.json
5+
custom/*
6+
tmp/*

api_client.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111

1212

1313
class APIClient:
14-
def __init__(self):
14+
def __init__(self, initials):
15+
self.initials = initials
1516
self.account_numbers = None
16-
self.config = APIConfig
17+
self.config = APIConfig(self.initials)
1718
self.session = requests.Session()
1819
self.setup_logging()
1920
self.token_info = self.load_token()
@@ -23,7 +24,7 @@ def __init__(self):
2324
self.manual_authorization_flow()
2425

2526
def setup_logging(self):
26-
logging.basicConfig(**APIConfig.LOGGING_CONFIG)
27+
logging.basicConfig(**self.config.LOGGING_CONFIG)
2728
self.logger = logging.getLogger(__name__)
2829

2930
def ensure_valid_token(self):
@@ -42,7 +43,7 @@ def ensure_valid_token(self):
4243
def manual_authorization_flow(self):
4344
""" Handle the manual steps required to get the authorization code from the user. """
4445
self.logger.info("Starting manual authorization flow.")
45-
auth_url = f"{APIConfig.API_BASE_URL}/v1/oauth/authorize?client_id={APIConfig.APP_KEY}&redirect_uri={APIConfig.CALLBACK_URL}&response_type=code"
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"
4647
webbrowser.open(auth_url)
4748
self.logger.info(f"Please authorize the application by visiting: {auth_url}")
4849
response_url = ColorPrint.input(
@@ -91,26 +92,26 @@ def refresh_access_token(self):
9192
def save_token(self, token_data):
9293
""" Save token data securely. """
9394
token_data['expires_at'] = (datetime.now() + timedelta(seconds=token_data['expires_in'])).isoformat()
94-
with open('token_data.json', 'w') as f:
95+
with open(f'schwab_token_data_{self.initials}.json', 'w') as f:
9596
json.dump(token_data, f)
9697
self.logger.info("Token data saved successfully.")
9798

9899
def load_token(self):
99100
""" Load token data. """
100101
try:
101-
with open('token_data.json', 'r') as f:
102+
with open(f'schwab_token_data_{self.initials}.json', 'r') as f:
102103
token_data = json.load(f)
103104
return token_data
104105
except Exception as e:
105106
self.logger.warning(f"Loading token failed: {e}")
106107
return None
107108

108109
def validate_token(self, force=False):
109-
""" Validate the current token's validity. """
110-
print(self.token_info['expires_at'])
111-
print(datetime.now())
112-
print(datetime.fromisoformat(self.token_info['expires_at']))
113-
print(datetime.now() < datetime.fromisoformat(self.token_info['expires_at']))
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']))
114115
if self.token_info and datetime.now() < datetime.fromisoformat(self.token_info['expires_at']):
115116
print(f"Token expires in {datetime.fromisoformat(self.token_info['expires_at']) - datetime.now()} seconds")
116117
return True

config.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,37 @@
22
from dotenv import load_dotenv
33

44
load_dotenv()
5+
SANDBOX = False
56

67

78
class APIConfig:
8-
API_BASE_URL = "https://api.schwabapi.com"
9-
TRADER_BASE_URL = f"{API_BASE_URL}/trader/v1"
10-
ACCOUNTS_BASE_URL = f"{TRADER_BASE_URL}/accounts"
11-
MARKET_DATA_BASE_URL = f"{API_BASE_URL}/marketdata/v1"
12-
ORDERS_BASE_URL = ACCOUNTS_BASE_URL
13-
STREAMER_INFO_URL = f"{API_BASE_URL}/streamer-info"
14-
REQUEST_TIMEOUT = 30 # Timeout for API requests in seconds
15-
RETRY_STRATEGY = {
16-
'total': 3, # Total number of retries to allow
17-
'backoff_factor': 1 # Factor by which the delay between retries will increase
18-
}
19-
TOKEN_REFRESH_THRESHOLD_SECONDS = 300 # Time in seconds before token expiration to attempt refresh
20-
DEBUG_MODE = False
21-
LOGGING_CONFIG = {
22-
'level': 'INFO',
23-
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
24-
}
25-
APP_KEY = os.getenv('APP_KEY')
26-
APP_SECRET = os.getenv('APP_SECRET')
27-
CALLBACK_URL = os.getenv('CALLBACK_URL')
9+
def __init__(self, initials):
10+
self.initials = initials
11+
if SANDBOX:
12+
self.API_BASE_URL = "http://localhost:4020"
13+
self.TRADER_BASE_URL = self.API_BASE_URL
14+
self.ACCOUNTS_BASE_URL = f"{self.API_BASE_URL}/accounts"
15+
self.MARKET_DATA_BASE_URL = f"{self.API_BASE_URL}/marketdata"
16+
self.ORDERS_BASE_URL = self.ACCOUNTS_BASE_URL
17+
self.STREAMER_INFO_URL = f"{self.API_BASE_URL}/streamer-info"
18+
else:
19+
self.API_BASE_URL = "https://api.schwabapi.com"
20+
self.TRADER_BASE_URL = f"{self.API_BASE_URL}/trader/v1"
21+
self.ACCOUNTS_BASE_URL = f"{self.TRADER_BASE_URL}/accounts"
22+
self.MARKET_DATA_BASE_URL = f"{self.API_BASE_URL}/marketdata/v1"
23+
self.ORDERS_BASE_URL = self.ACCOUNTS_BASE_URL
24+
self.STREAMER_INFO_URL = f"{self.API_BASE_URL}/streamer-info"
25+
self.REQUEST_TIMEOUT = 30 # Timeout for API requests in seconds
26+
self. RETRY_STRATEGY = {
27+
'total': 3, # Total number of retries to allow
28+
'backoff_factor': 1 # Factor by which the delay between retries will increase
29+
}
30+
self.TOKEN_REFRESH_THRESHOLD_SECONDS = 300 # Time in seconds before token expiration to attempt refresh
31+
self.DEBUG_MODE = False
32+
self.LOGGING_CONFIG = {
33+
'level': 'INFO',
34+
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
35+
}
36+
self.APP_KEY = os.getenv(f'SCHWAB_APP_KEY_{self.initials}')
37+
self.APP_SECRET = os.getenv(f'SCHWAB_APP_SECRET_{self.initials}')
38+
self.CALLBACK_URL = os.getenv('CALLBACK_URL')

main.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,33 @@ async def main_stream():
3434

3535
stream_client.stop()
3636

37+
3738
def main():
3839
client = APIClient() # Initialize the API client
3940
accounts_api = Accounts(client)
4041
orders_api = Orders(client)
4142

4243
# Get account numbers for linked accounts
43-
# print(accounts_api.get_account_numbers()) # working
44+
client.account_numbers = accounts_api.get_account_numbers() # working
45+
print(client.account_numbers)
4446

4547
# Get positions for linked accounts
46-
# print(accounts_api.get_all_accounts()) # working
48+
print(accounts_api.get_all_accounts()) # working
4749

48-
sample_account = client.account_numbers[0]
49-
account_hash = sample_account['accountHash']
50+
sample_account = client.account_numbers[0] # working
51+
print(sample_account)
52+
account_hash = sample_account['hashValue'] # working
53+
print(account_hash)
5054

5155
# Get specific account positions
52-
# print(accounts_api.get_account(fields="positions"))
56+
print(accounts_api.get_account(account_hash=account_hash, fields="positions"))
5357

5458
# Get up to 3000 orders for an account for the past week
55-
print(orders_api.get_orders(3000, datetime.now() - timedelta(days=7), datetime.now()).json())
59+
print(orders_api.get_orders(account_hash=account_hash,
60+
max_results=3000,
61+
from_entered_time=datetime.now() - timedelta(days=7),
62+
to_entered_time=datetime.now())
63+
)
5664

5765
# Example to place an order (commented out for safety)
5866
"""
@@ -75,11 +83,16 @@ def main():
7583
# print(orders_api.get_order('account_hash', order_id).json())
7684

7785
# Get up to 3000 orders for all accounts for the past week
78-
print(orders_api.get_orders(account_hash=account_hash, max_results=3000, from_entered_time=datetime.now() - timedelta(days=7), to_entered_time=datetime.now()))
86+
for account in client.account_numbers:
87+
account_hash = account['hashValue']
88+
print(orders_api.get_orders(account_hash=account_hash, max_results=3000, from_entered_time=datetime.now() - timedelta(days=7), to_entered_time=datetime.now()))
7989

8090
# Get all transactions for an account
81-
print(accounts_api.get_account_transactions('account_hash', datetime.now() - timedelta(days=7), datetime.now(),
82-
"TRADE").json())
91+
print(accounts_api.get_account_transactions(account_hash=account_hash,
92+
start_date=datetime.now() - timedelta(days=7),
93+
end_date=datetime.now(),
94+
types="TRADE")
95+
)
8396

8497
# Market-data-related requests
8598
quotes = Quotes(client)
@@ -98,6 +111,9 @@ def main():
98111
# Get an option expiration chain
99112
print(options.get_chains("AAPL").json())
100113

114+
# Get price history for a symbol
115+
print(price_history.by_symbol("AAPL", period_type="day", period=1, frequency_type="minute", frequency=5).json())
116+
101117
# Get movers for an index
102118
print(movers.get_movers("$DJI").json())
103119

@@ -117,6 +133,6 @@ def main():
117133
if __name__ == '__main__':
118134
print("Welcome to the unofficial Schwab API interface!\n"
119135
"GitHub: https://github.com/Patch-Code-Prosperity/Pythonic-Schwab-API")
120-
loop = get_event_loop()
121-
loop.run_until_complete(main_stream())
122-
# main()
136+
# loop = get_event_loop()
137+
# loop.run_until_complete(main_stream())
138+
main()

orders.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1+
from datetime import datetime, timezone, timedelta
2+
3+
14
class Orders:
25
def __init__(self, client):
36
self.client = client
47
self.base_url = client.config.ORDERS_BASE_URL
58

69
def get_orders(self, account_hash, max_results=100, from_entered_time=None, to_entered_time=None, status=None):
710
"""Retrieve a list of orders for a specified account."""
11+
if from_entered_time is None:
12+
from_entered_time = (datetime.now(timezone.utc) - timedelta(days=364)).isoformat(timespec='seconds')
13+
if to_entered_time is None:
14+
to_entered_time = datetime.now(timezone.utc).isoformat(timespec='seconds')
815
params = {
916
'maxResults': max_results,
10-
'fromEnteredTime': from_entered_time.isoformat() if from_entered_time else None,
11-
'toEnteredTime': to_entered_time.isoformat() if to_entered_time else None,
17+
'fromEnteredTime': from_entered_time,
18+
'toEnteredTime': to_entered_time,
1219
'status': status
1320
}
1421
endpoint = f"{self.base_url}/{account_hash}/orders"
@@ -27,9 +34,9 @@ def get_order(self, account_hash, order_id):
2734
def cancel_order(self, account_hash, order_id):
2835
"""Cancel a specific order."""
2936
endpoint = f"{self.base_url}/{account_hash}/orders/{order_id}"
30-
return self.client.delete(endpoint)
37+
return self.client.make_request(endpoint, method='DELETE')
3138

3239
def replace_order(self, account_hash, order_id, new_order_details):
3340
"""Replace an existing order with new details."""
3441
endpoint = f"{self.base_url}/{account_hash}/orders/{order_id}"
35-
return self.client.put(endpoint, data=new_order_details)
42+
return self.client.make_request(endpoint, method='PUT', data=new_order_details)

0 commit comments

Comments
 (0)