firstrade 0.0.21__py3-none-any.whl → 0.0.32__py3-none-any.whl
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.
- firstrade/account.py +294 -164
- firstrade/exceptions.py +55 -0
- firstrade/order.py +163 -187
- firstrade/symbols.py +146 -51
- firstrade/urls.py +58 -24
- {firstrade-0.0.21.dist-info → firstrade-0.0.32.dist-info}/METADATA +39 -15
- firstrade-0.0.32.dist-info/RECORD +11 -0
- {firstrade-0.0.21.dist-info → firstrade-0.0.32.dist-info}/WHEEL +1 -1
- firstrade-0.0.21.dist-info/RECORD +0 -10
- {firstrade-0.0.21.dist-info → firstrade-0.0.32.dist-info}/LICENSE +0 -0
- {firstrade-0.0.21.dist-info → firstrade-0.0.32.dist-info}/top_level.txt +0 -0
firstrade/account.py
CHANGED
|
@@ -1,17 +1,67 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
3
|
import pickle
|
|
3
|
-
import re
|
|
4
4
|
|
|
5
|
+
import pyotp
|
|
5
6
|
import requests
|
|
6
|
-
from bs4 import BeautifulSoup
|
|
7
7
|
|
|
8
8
|
from firstrade import urls
|
|
9
|
+
from firstrade.exceptions import (
|
|
10
|
+
AccountResponseError,
|
|
11
|
+
LoginRequestError,
|
|
12
|
+
LoginResponseError,
|
|
13
|
+
)
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class FTSession:
|
|
12
|
-
"""
|
|
17
|
+
"""
|
|
18
|
+
Class creating a session for Firstrade.
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
This class handles the creation and management of a session for logging into the Firstrade platform.
|
|
21
|
+
It supports multi-factor authentication (MFA) and can save session cookies for persistent logins.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
username (str): Firstrade login username.
|
|
25
|
+
password (str): Firstrade login password.
|
|
26
|
+
pin (str): Firstrade login pin.
|
|
27
|
+
email (str, optional): Firstrade MFA email.
|
|
28
|
+
phone (str, optional): Firstrade MFA phone number.
|
|
29
|
+
mfa_secret (str, optional): Secret key for generating MFA codes.
|
|
30
|
+
profile_path (str, optional): The path where the user wants to save the cookie pkl file.
|
|
31
|
+
t_token (str, optional): Token used for MFA.
|
|
32
|
+
otp_options (dict, optional): Options for OTP (One-Time Password) if MFA is enabled.
|
|
33
|
+
login_json (dict, optional): JSON response from the login request.
|
|
34
|
+
session (requests.Session): The requests session object used for making HTTP requests.
|
|
35
|
+
|
|
36
|
+
Methods:
|
|
37
|
+
__init__(username, password, pin=None, email=None, phone=None, mfa_secret=None, profile_path=None):
|
|
38
|
+
Initializes a new instance of the FTSession class.
|
|
39
|
+
login():
|
|
40
|
+
Validates and logs into the Firstrade platform.
|
|
41
|
+
login_two(code):
|
|
42
|
+
Finishes the login process to the Firstrade platform. When using email or phone mfa.
|
|
43
|
+
delete_cookies():
|
|
44
|
+
Deletes the session cookies.
|
|
45
|
+
_load_cookies():
|
|
46
|
+
Checks if session cookies were saved and loads them.
|
|
47
|
+
_save_cookies():
|
|
48
|
+
Saves session cookies to a file.
|
|
49
|
+
_mask_email(email):
|
|
50
|
+
Masks the email for use in the API.
|
|
51
|
+
_handle_mfa():
|
|
52
|
+
Handles multi-factor authentication.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
username,
|
|
58
|
+
password,
|
|
59
|
+
pin=None,
|
|
60
|
+
email=None,
|
|
61
|
+
phone=None,
|
|
62
|
+
mfa_secret=None,
|
|
63
|
+
profile_path=None,
|
|
64
|
+
):
|
|
15
65
|
"""
|
|
16
66
|
Initializes a new instance of the FTSession class.
|
|
17
67
|
|
|
@@ -19,76 +69,113 @@ class FTSession:
|
|
|
19
69
|
username (str): Firstrade login username.
|
|
20
70
|
password (str): Firstrade login password.
|
|
21
71
|
pin (str): Firstrade login pin.
|
|
22
|
-
|
|
72
|
+
email (str, optional): Firstrade MFA email.
|
|
73
|
+
phone (str, optional): Firstrade MFA phone number.
|
|
23
74
|
profile_path (str, optional): The path where the user wants to save the cookie pkl file.
|
|
24
75
|
"""
|
|
25
76
|
self.username = username
|
|
26
77
|
self.password = password
|
|
27
78
|
self.pin = pin
|
|
79
|
+
self.email = FTSession._mask_email(email) if email is not None else None
|
|
80
|
+
self.phone = phone
|
|
81
|
+
self.mfa_secret = mfa_secret
|
|
28
82
|
self.profile_path = profile_path
|
|
83
|
+
self.t_token = None
|
|
84
|
+
self.otp_options = None
|
|
85
|
+
self.login_json = None
|
|
29
86
|
self.session = requests.Session()
|
|
30
|
-
self.login()
|
|
31
87
|
|
|
32
88
|
def login(self):
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
cookies = self.load_cookies()
|
|
36
|
-
cookies = requests.utils.cookiejar_from_dict(cookies)
|
|
37
|
-
self.session.cookies.update(cookies)
|
|
38
|
-
response = self.session.get(
|
|
39
|
-
url=urls.get_xml(), headers=urls.session_headers(), cookies=cookies
|
|
40
|
-
)
|
|
41
|
-
if response.status_code != 200:
|
|
42
|
-
raise Exception(
|
|
43
|
-
"Login failed. Check your credentials or internet connection."
|
|
44
|
-
)
|
|
45
|
-
if "/cgi-bin/sessionfailed?reason=6" in response.text:
|
|
46
|
-
self.session.get(url=urls.login(), headers=headers)
|
|
47
|
-
data = {
|
|
48
|
-
"redirect": "",
|
|
49
|
-
"ft_locale": "en-us",
|
|
50
|
-
"login.x": "Log In",
|
|
51
|
-
"username": r"" + self.username,
|
|
52
|
-
"password": r"" + self.password,
|
|
53
|
-
"destination_page": "home",
|
|
54
|
-
}
|
|
89
|
+
"""
|
|
90
|
+
Validates and logs into the Firstrade platform.
|
|
55
91
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
92
|
+
This method sets up the session headers, loads cookies if available, and performs the login request.
|
|
93
|
+
It handles multi-factor authentication (MFA) if required.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
LoginRequestError: If the login request fails with a non-200 status code.
|
|
97
|
+
LoginResponseError: If the login response contains an error message.
|
|
98
|
+
"""
|
|
99
|
+
self.session.headers = urls.session_headers()
|
|
100
|
+
ftat = self._load_cookies()
|
|
101
|
+
if ftat != "":
|
|
102
|
+
self.session.headers["ftat"] = ftat
|
|
103
|
+
response = self.session.get(url="https://api3x.firstrade.com/", timeout=10)
|
|
104
|
+
self.session.headers["access-token"] = urls.access_token()
|
|
105
|
+
|
|
106
|
+
data = {
|
|
107
|
+
"username": r"" + self.username,
|
|
108
|
+
"password": r"" + self.password,
|
|
109
|
+
}
|
|
69
110
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
111
|
+
response = self.session.post(
|
|
112
|
+
url=urls.login(),
|
|
113
|
+
data=data,
|
|
114
|
+
)
|
|
115
|
+
try:
|
|
116
|
+
self.login_json = response.json()
|
|
117
|
+
except json.decoder.JSONDecodeError:
|
|
118
|
+
raise LoginResponseError("Invalid JSON is your account funded?")
|
|
74
119
|
if (
|
|
75
|
-
"
|
|
76
|
-
in self.
|
|
77
|
-
|
|
78
|
-
).text
|
|
120
|
+
"mfa" not in self.login_json
|
|
121
|
+
and "ftat" in self.login_json
|
|
122
|
+
and self.login_json["error"] == ""
|
|
79
123
|
):
|
|
80
|
-
|
|
124
|
+
self.session.headers["sid"] = self.login_json["sid"]
|
|
125
|
+
return False
|
|
126
|
+
self.t_token = self.login_json.get("t_token")
|
|
127
|
+
if self.mfa_secret is None:
|
|
128
|
+
self.otp_options = self.login_json.get("otp")
|
|
129
|
+
if response.status_code != 200:
|
|
130
|
+
raise LoginRequestError(response.status_code)
|
|
131
|
+
if self.login_json["error"] != "":
|
|
132
|
+
raise LoginResponseError(self.login_json["error"])
|
|
133
|
+
need_code = self._handle_mfa()
|
|
134
|
+
if self.login_json["error"] != "":
|
|
135
|
+
raise LoginResponseError(self.login_json["error"])
|
|
136
|
+
if need_code:
|
|
137
|
+
return True
|
|
138
|
+
self.session.headers["ftat"] = self.login_json["ftat"]
|
|
139
|
+
self.session.headers["sid"] = self.login_json["sid"]
|
|
140
|
+
self._save_cookies()
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
def login_two(self, code):
|
|
144
|
+
"""Method to finish login to the Firstrade platform."""
|
|
145
|
+
data = {
|
|
146
|
+
"otpCode": code,
|
|
147
|
+
"verificationSid": self.session.headers["sid"],
|
|
148
|
+
"remember_for": "30",
|
|
149
|
+
"t_token": self.t_token,
|
|
150
|
+
}
|
|
151
|
+
response = self.session.post(urls.verify_pin(), data=data)
|
|
152
|
+
self.login_json = response.json()
|
|
153
|
+
if self.login_json["error"] != "":
|
|
154
|
+
raise LoginResponseError(self.login_json["error"])
|
|
155
|
+
self.session.headers["ftat"] = self.login_json["ftat"]
|
|
156
|
+
self.session.headers["sid"] = self.login_json["sid"]
|
|
157
|
+
self._save_cookies()
|
|
81
158
|
|
|
82
|
-
def
|
|
159
|
+
def delete_cookies(self):
|
|
160
|
+
"""Deletes the session cookies."""
|
|
161
|
+
if self.profile_path is not None:
|
|
162
|
+
path = os.path.join(self.profile_path, f"ft_cookies{self.username}.pkl")
|
|
163
|
+
else:
|
|
164
|
+
path = f"ft_cookies{self.username}.pkl"
|
|
165
|
+
os.remove(path)
|
|
166
|
+
|
|
167
|
+
def _load_cookies(self):
|
|
83
168
|
"""
|
|
84
169
|
Checks if session cookies were saved.
|
|
85
170
|
|
|
86
171
|
Returns:
|
|
87
|
-
|
|
172
|
+
str: The saved session token.
|
|
88
173
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
174
|
+
|
|
175
|
+
ftat = ""
|
|
176
|
+
directory = (
|
|
177
|
+
os.path.abspath(self.profile_path) if self.profile_path is not None else "."
|
|
178
|
+
)
|
|
92
179
|
if not os.path.exists(directory):
|
|
93
180
|
os.makedirs(directory)
|
|
94
181
|
|
|
@@ -96,10 +183,10 @@ class FTSession:
|
|
|
96
183
|
if filename.endswith(f"{self.username}.pkl"):
|
|
97
184
|
filepath = os.path.join(directory, filename)
|
|
98
185
|
with open(filepath, "rb") as f:
|
|
99
|
-
|
|
100
|
-
return
|
|
186
|
+
ftat = pickle.load(f)
|
|
187
|
+
return ftat
|
|
101
188
|
|
|
102
|
-
def
|
|
189
|
+
def _save_cookies(self):
|
|
103
190
|
"""Saves session cookies to a file."""
|
|
104
191
|
if self.profile_path is not None:
|
|
105
192
|
directory = os.path.abspath(self.profile_path)
|
|
@@ -109,15 +196,76 @@ class FTSession:
|
|
|
109
196
|
else:
|
|
110
197
|
path = f"ft_cookies{self.username}.pkl"
|
|
111
198
|
with open(path, "wb") as f:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
199
|
+
ftat = self.session.headers.get("ftat")
|
|
200
|
+
pickle.dump(ftat, f)
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _mask_email(email):
|
|
204
|
+
"""
|
|
205
|
+
Masks the email for use in the API.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
email (str): The email address to be masked.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
str: The masked email address.
|
|
212
|
+
"""
|
|
213
|
+
local, domain = email.split("@")
|
|
214
|
+
masked_local = local[0] + "*" * 4
|
|
215
|
+
domain_name, tld = domain.split(".")
|
|
216
|
+
masked_domain = domain_name[0] + "*" * 4
|
|
217
|
+
return f"{masked_local}@{masked_domain}.{tld}"
|
|
218
|
+
|
|
219
|
+
def _handle_mfa(self):
|
|
220
|
+
"""
|
|
221
|
+
Handles multi-factor authentication.
|
|
222
|
+
|
|
223
|
+
This method processes the MFA requirements based on the login response and user-provided details.
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
LoginRequestError: If the MFA request fails with a non-200 status code.
|
|
227
|
+
LoginResponseError: If the MFA response contains an error message.
|
|
228
|
+
"""
|
|
229
|
+
if not self.login_json["mfa"] and self.pin is not None:
|
|
230
|
+
data = {
|
|
231
|
+
"pin": self.pin,
|
|
232
|
+
"remember_for": "30",
|
|
233
|
+
"t_token": self.t_token,
|
|
234
|
+
}
|
|
235
|
+
response = self.session.post(urls.verify_pin(), data=data)
|
|
236
|
+
self.login_json = response.json()
|
|
237
|
+
elif not self.login_json["mfa"] and (
|
|
238
|
+
self.email is not None or self.phone is not None
|
|
239
|
+
):
|
|
240
|
+
for item in self.otp_options:
|
|
241
|
+
if item["channel"] == "sms" and self.phone is not None:
|
|
242
|
+
if self.phone in item["recipientMask"]:
|
|
243
|
+
data = {
|
|
244
|
+
"recipientId": item["recipientId"],
|
|
245
|
+
"t_token": self.t_token,
|
|
246
|
+
}
|
|
247
|
+
elif item["channel"] == "email" and self.email is not None:
|
|
248
|
+
if self.email == item["recipientMask"]:
|
|
249
|
+
data = {
|
|
250
|
+
"recipientId": item["recipientId"],
|
|
251
|
+
"t_token": self.t_token,
|
|
252
|
+
}
|
|
253
|
+
response = self.session.post(urls.request_code(), data=data)
|
|
254
|
+
elif self.login_json["mfa"] and self.mfa_secret is not None:
|
|
255
|
+
mfa_otp = pyotp.TOTP(self.mfa_secret).now()
|
|
256
|
+
data = {
|
|
257
|
+
"mfaCode": mfa_otp,
|
|
258
|
+
"remember_for": "30",
|
|
259
|
+
"t_token": self.t_token,
|
|
260
|
+
}
|
|
261
|
+
response = self.session.post(urls.verify_pin(), data=data)
|
|
262
|
+
self.login_json = response.json()
|
|
263
|
+
if self.login_json["error"] == "":
|
|
264
|
+
if self.pin or self.mfa_secret is not None:
|
|
265
|
+
self.session.headers["sid"] = self.login_json["sid"]
|
|
266
|
+
return False
|
|
267
|
+
self.session.headers["sid"] = self.login_json["verificationSid"]
|
|
268
|
+
return True
|
|
121
269
|
|
|
122
270
|
def __getattr__(self, name):
|
|
123
271
|
"""
|
|
@@ -146,74 +294,28 @@ class FTAccountData:
|
|
|
146
294
|
self.session = session
|
|
147
295
|
self.all_accounts = []
|
|
148
296
|
self.account_numbers = []
|
|
149
|
-
self.
|
|
150
|
-
self.
|
|
151
|
-
self.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
headers=urls.session_headers(),
|
|
172
|
-
cookies=self.session.cookies,
|
|
173
|
-
data=data,
|
|
174
|
-
)
|
|
175
|
-
# request to get account status data
|
|
176
|
-
data = {"req": "get_status"}
|
|
177
|
-
account_status = self.session.post(
|
|
178
|
-
url=urls.status(),
|
|
179
|
-
headers=urls.session_headers(),
|
|
180
|
-
cookies=self.session.cookies,
|
|
181
|
-
data=data,
|
|
182
|
-
).json()
|
|
183
|
-
self.account_statuses.append(account_status["data"])
|
|
184
|
-
data = {"page": "bal", "account_id": account}
|
|
185
|
-
account_soup = BeautifulSoup(
|
|
186
|
-
self.session.post(
|
|
187
|
-
url=urls.get_xml(),
|
|
188
|
-
headers=urls.session_headers(),
|
|
189
|
-
cookies=self.session.cookies,
|
|
190
|
-
data=data,
|
|
191
|
-
).text,
|
|
192
|
-
"xml",
|
|
193
|
-
)
|
|
194
|
-
balance = account_soup.find("total_account_value").text
|
|
195
|
-
self.account_balances.append(balance)
|
|
196
|
-
all_account_info.append(
|
|
197
|
-
{
|
|
198
|
-
account: {
|
|
199
|
-
"Balance": balance,
|
|
200
|
-
"Status": {
|
|
201
|
-
"primary": account_status["data"]["primary"],
|
|
202
|
-
"domestic": account_status["data"]["domestic"],
|
|
203
|
-
"joint": account_status["data"]["joint"],
|
|
204
|
-
"ira": account_status["data"]["ira"],
|
|
205
|
-
"hasMargin": account_status["data"]["hasMargin"],
|
|
206
|
-
"opLevel": account_status["data"]["opLevel"],
|
|
207
|
-
"p_country": account_status["data"]["p_country"],
|
|
208
|
-
"mrgnStatus": account_status["data"]["mrgnStatus"],
|
|
209
|
-
"opStatus": account_status["data"]["opStatus"],
|
|
210
|
-
"margin_id": account_status["data"]["margin_id"],
|
|
211
|
-
},
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
self.all_accounts = all_account_info
|
|
297
|
+
self.account_balances = {}
|
|
298
|
+
response = self.session.get(url=urls.user_info())
|
|
299
|
+
self.user_info = response.json()
|
|
300
|
+
response = self.session.get(urls.account_list())
|
|
301
|
+
if response.status_code != 200 or response.json()["error"] != "":
|
|
302
|
+
raise AccountResponseError(response.json()["error"])
|
|
303
|
+
self.all_accounts = response.json()
|
|
304
|
+
for item in self.all_accounts["items"]:
|
|
305
|
+
self.account_numbers.append(item["account"])
|
|
306
|
+
self.account_balances[item["account"]] = item["total_value"]
|
|
307
|
+
|
|
308
|
+
def get_account_balances(self, account):
|
|
309
|
+
"""Gets account balances for a given account.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
account (str): Account number of the account you want to get balances for.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
dict: Dict of the response from the API.
|
|
316
|
+
"""
|
|
317
|
+
response = self.session.get(urls.account_balances(account))
|
|
318
|
+
return response.json()
|
|
217
319
|
|
|
218
320
|
def get_positions(self, account):
|
|
219
321
|
"""Gets currently held positions for a given account.
|
|
@@ -222,36 +324,64 @@ class FTAccountData:
|
|
|
222
324
|
account (str): Account number of the account you want to get positions for.
|
|
223
325
|
|
|
224
326
|
Returns:
|
|
225
|
-
|
|
226
|
-
|
|
327
|
+
dict: Dict of the response from the API.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
response = self.session.get(urls.account_positions(account))
|
|
331
|
+
return response.json()
|
|
332
|
+
|
|
333
|
+
def get_account_history(self, account, date_range="ytd", custom_range=None):
|
|
334
|
+
"""Gets account history for a given account.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
account (str): Account number of the account you want to get history for.
|
|
338
|
+
range (str): The range of the history. Defaults to "ytd".
|
|
339
|
+
Available options are
|
|
340
|
+
["today", "1w", "1m", "2m", "mtd", "ytd", "ly", "cust"].
|
|
341
|
+
custom_range (str): The custom range of the history.
|
|
342
|
+
Defaults to None. If range is "cust",
|
|
343
|
+
this parameter is required.
|
|
344
|
+
Format: ["YYYY-MM-DD", "YYYY-MM-DD"].
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
dict: Dict of the response from the API.
|
|
348
|
+
"""
|
|
349
|
+
if date_range == "cust" and custom_range is None:
|
|
350
|
+
raise ValueError("Custom range is required when date_range is 'cust'.")
|
|
351
|
+
response = self.session.get(
|
|
352
|
+
urls.account_history(account, date_range, custom_range)
|
|
353
|
+
)
|
|
354
|
+
return response.json()
|
|
355
|
+
|
|
356
|
+
def get_orders(self, account):
|
|
357
|
+
"""
|
|
358
|
+
Retrieves existing order data for a given account.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
ft_session (FTSession): The session object used for making HTTP requests to Firstrade.
|
|
362
|
+
account (str): Account number of the account to retrieve orders for.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
list: A list of dictionaries, each containing details about an order.
|
|
227
366
|
"""
|
|
367
|
+
|
|
368
|
+
response = self.session.get(url=urls.order_list(account))
|
|
369
|
+
return response.json()
|
|
370
|
+
|
|
371
|
+
def cancel_order(self, order_id):
|
|
372
|
+
"""
|
|
373
|
+
Cancels an existing order.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
order_id (str): The order ID to cancel.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
dict: A dictionary containing the response data.
|
|
380
|
+
"""
|
|
381
|
+
|
|
228
382
|
data = {
|
|
229
|
-
"
|
|
230
|
-
"accountId": str(account),
|
|
383
|
+
"order_id": order_id,
|
|
231
384
|
}
|
|
232
|
-
position_soup = BeautifulSoup(
|
|
233
|
-
self.session.post(
|
|
234
|
-
url=urls.get_xml(),
|
|
235
|
-
headers=urls.session_headers(),
|
|
236
|
-
data=data,
|
|
237
|
-
cookies=self.session.cookies,
|
|
238
|
-
).text,
|
|
239
|
-
"xml",
|
|
240
|
-
)
|
|
241
385
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
price = position_soup.find_all("price")
|
|
245
|
-
change = position_soup.find_all("change")
|
|
246
|
-
change_percent = position_soup.find_all("changepercent")
|
|
247
|
-
vol = position_soup.find_all("vol")
|
|
248
|
-
for i, ticker in enumerate(tickers):
|
|
249
|
-
ticker = ticker.text
|
|
250
|
-
self.securities_held[ticker] = {
|
|
251
|
-
"quantity": quantity[i].text,
|
|
252
|
-
"price": price[i].text,
|
|
253
|
-
"change": change[i].text,
|
|
254
|
-
"change_percent": change_percent[i].text,
|
|
255
|
-
"vol": vol[i].text,
|
|
256
|
-
}
|
|
257
|
-
return self.securities_held
|
|
386
|
+
response = self.session.post(url=urls.cancel_order(), data=data)
|
|
387
|
+
return response.json()
|
firstrade/exceptions.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
class QuoteError(Exception):
|
|
2
|
+
"""Base class for exceptions in the Quote module."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class QuoteRequestError(QuoteError):
|
|
6
|
+
"""Exception raised for errors in the HTTP request during a Quote."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, status_code, message="Error in HTTP request"):
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
self.message = f"{message}. HTTP status code: {status_code}"
|
|
11
|
+
super().__init__(self.message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QuoteResponseError(QuoteError):
|
|
15
|
+
"""Exception raised for errors in the API response."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, symbol, error_message):
|
|
18
|
+
self.symbol = symbol
|
|
19
|
+
self.message = f"Failed to get data for {symbol}. API returned the following error: {error_message}"
|
|
20
|
+
super().__init__(self.message)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LoginError(Exception):
|
|
24
|
+
"""Exception raised for errors in the login process."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LoginRequestError(LoginError):
|
|
28
|
+
"""Exception raised for errors in the HTTP request during login."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, status_code, message="Error in HTTP request during login"):
|
|
31
|
+
self.status_code = status_code
|
|
32
|
+
self.message = f"{message}. HTTP status code: {status_code}"
|
|
33
|
+
super().__init__(self.message)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LoginResponseError(LoginError):
|
|
37
|
+
"""Exception raised for errors in the API response during login."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, error_message):
|
|
40
|
+
self.message = (
|
|
41
|
+
f"Failed to login. API returned the following error: {error_message}"
|
|
42
|
+
)
|
|
43
|
+
super().__init__(self.message)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AccountError(Exception):
|
|
47
|
+
"""Base class for exceptions in the Account module."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AccountResponseError(AccountError):
|
|
51
|
+
"""Exception raised for errors in the API response when getting account data."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, error_message):
|
|
54
|
+
self.message = f"Failed to get account data. API returned the following error: {error_message}"
|
|
55
|
+
super().__init__(self.message)
|