firstrade 0.0.34__py3-none-any.whl → 0.0.37__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 CHANGED
@@ -1,21 +1,22 @@
1
1
  import json
2
- import os
3
- import pickle
2
+ from pathlib import Path
4
3
 
5
4
  import pyotp
5
+ import logging
6
6
  import requests
7
7
 
8
8
  from firstrade import urls
9
9
  from firstrade.exceptions import (
10
10
  AccountResponseError,
11
+ LoginError,
11
12
  LoginRequestError,
12
13
  LoginResponseError,
13
14
  )
14
15
 
16
+ logger = logging.getLogger(__name__)
15
17
 
16
18
  class FTSession:
17
- """
18
- Class creating a session for Firstrade.
19
+ """Class creating a session for Firstrade.
19
20
 
20
21
  This class handles the creation and management of a session for logging into the Firstrade platform.
21
22
  It supports multi-factor authentication (MFA) and can save session cookies for persistent logins.
@@ -23,18 +24,19 @@ class FTSession:
23
24
  Attributes:
24
25
  username (str): Firstrade login username.
25
26
  password (str): Firstrade login password.
26
- pin (str): Firstrade login pin.
27
+ pin (str, optional): Firstrade login pin.
27
28
  email (str, optional): Firstrade MFA email.
28
29
  phone (str, optional): Firstrade MFA phone number.
29
30
  mfa_secret (str, optional): Secret key for generating MFA codes.
30
31
  profile_path (str, optional): The path where the user wants to save the cookie pkl file.
32
+ debug (bool, optional): Log HTTP requests/responses if true. DO NOT POST YOUR LOGS ONLINE.
31
33
  t_token (str, optional): Token used for MFA.
32
34
  otp_options (dict, optional): Options for OTP (One-Time Password) if MFA is enabled.
33
35
  login_json (dict, optional): JSON response from the login request.
34
36
  session (requests.Session): The requests session object used for making HTTP requests.
35
37
 
36
38
  Methods:
37
- __init__(username, password, pin=None, email=None, phone=None, mfa_secret=None, profile_path=None):
39
+ __init__(username, password, pin=None, email=None, phone=None, mfa_secret=None, profile_path=None, debug=False):
38
40
  Initializes a new instance of the FTSession class.
39
41
  login():
40
42
  Validates and logs into the Firstrade platform.
@@ -50,44 +52,58 @@ class FTSession:
50
52
  Masks the email for use in the API.
51
53
  _handle_mfa():
52
54
  Handles multi-factor authentication.
55
+ _request(method, url, **kwargs):
56
+ HTTP requests wrapper to the API.
57
+
53
58
  """
54
59
 
55
60
  def __init__(
56
61
  self,
57
- username,
58
- password,
59
- pin=None,
60
- email=None,
61
- phone=None,
62
- mfa_secret=None,
63
- profile_path=None,
64
- ):
65
- """
66
- Initializes a new instance of the FTSession class.
62
+ username: str,
63
+ password: str,
64
+ pin: str = "",
65
+ email: str = "",
66
+ phone: str = "",
67
+ mfa_secret: str = "",
68
+ profile_path: str | None = None,
69
+ debug: bool = False
70
+ ) -> None:
71
+ """Initialize a new instance of the FTSession class.
67
72
 
68
73
  Args:
69
74
  username (str): Firstrade login username.
70
75
  password (str): Firstrade login password.
71
- pin (str): Firstrade login pin.
76
+ pin (str, optional): Firstrade login pin.
72
77
  email (str, optional): Firstrade MFA email.
73
78
  phone (str, optional): Firstrade MFA phone number.
79
+ mfa_secret (str, optional): Firstrade MFA secret key to generate TOTP.
74
80
  profile_path (str, optional): The path where the user wants to save the cookie pkl file.
81
+ debug (bool, optional): Log HTTP requests/responses if true. DO NOT POST YOUR LOGS ONLINE.
82
+
75
83
  """
76
- self.username = username
77
- self.password = password
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
82
- self.profile_path = profile_path
83
- self.t_token = None
84
- self.otp_options = None
85
- self.login_json = None
84
+ self.username: str = username
85
+ self.password: str = password
86
+ self.pin: str = pin
87
+ self.email: str = FTSession._mask_email(email) if email else ""
88
+ self.phone: str = phone
89
+ self.mfa_secret: str = mfa_secret
90
+ self.profile_path: str | None = profile_path
91
+ self.debug: bool = debug
92
+ if self.debug:
93
+ logging.basicConfig(level=logging.DEBUG)
94
+ # Enable HTTP connection debug output
95
+ import http.client as http_client
96
+ http_client.HTTPConnection.debuglevel = 1
97
+ # requests logging too
98
+ logging.getLogger("requests.packages.urllib3").setLevel(logging.DEBUG)
99
+ logging.getLogger("requests.packages.urllib3").propagate = True
100
+ self.t_token: str | None = None
101
+ self.otp_options: list[dict[str, str]] | None = None
102
+ self.login_json: dict[str, str] = {}
86
103
  self.session = requests.Session()
87
104
 
88
- def login(self):
89
- """
90
- Validates and logs into the Firstrade platform.
105
+ def login(self) -> bool:
106
+ """Validate and log into the Firstrade platform.
91
107
 
92
108
  This method sets up the session headers, loads cookies if available, and performs the login request.
93
109
  It handles multi-factor authentication (MFA) if required.
@@ -95,43 +111,42 @@ class FTSession:
95
111
  Raises:
96
112
  LoginRequestError: If the login request fails with a non-200 status code.
97
113
  LoginResponseError: If the login response contains an error message.
114
+
98
115
  """
99
- self.session.headers = urls.session_headers()
100
- ftat = self._load_cookies()
101
- if ftat != "":
116
+ self.session.headers.update(urls.session_headers())
117
+ ftat: str = self._load_cookies()
118
+ if ftat:
102
119
  self.session.headers["ftat"] = ftat
103
- response = self.session.get(url="https://api3x.firstrade.com/", timeout=10)
120
+ response: requests.Response = self._request("get", url="https://api3x.firstrade.com/", timeout=10)
104
121
  self.session.headers["access-token"] = urls.access_token()
105
122
 
106
- data = {
123
+ data: dict[str, str] = {
107
124
  "username": r"" + self.username,
108
125
  "password": r"" + self.password,
109
126
  }
110
127
 
111
- response = self.session.post(
128
+ response: requests.Response = self._request(
129
+ "post",
112
130
  url=urls.login(),
113
131
  data=data,
114
132
  )
115
133
  try:
116
- self.login_json = response.json()
117
- except json.decoder.JSONDecodeError:
118
- raise LoginResponseError("Invalid JSON is your account funded?")
119
- if (
120
- "mfa" not in self.login_json
121
- and "ftat" in self.login_json
122
- and self.login_json["error"] == ""
123
- ):
134
+ self.login_json: dict[str, str] = response.json()
135
+ except json.decoder.JSONDecodeError as exc:
136
+ error_msg = "Invalid JSON is your account funded?"
137
+ raise LoginResponseError(error_msg) from exc
138
+ if "mfa" not in self.login_json and "ftat" in self.login_json and not self.login_json["error"]:
124
139
  self.session.headers["sid"] = self.login_json["sid"]
125
140
  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")
141
+ self.t_token: str | None = self.login_json.get("t_token")
142
+ if not self.mfa_secret:
143
+ self.otp_options: str | None = self.login_json.get("otp")
129
144
  if response.status_code != 200:
130
145
  raise LoginRequestError(response.status_code)
131
- if self.login_json["error"] != "":
146
+ if self.login_json["error"]:
132
147
  raise LoginResponseError(self.login_json["error"])
133
- need_code = self._handle_mfa()
134
- if self.login_json["error"] != "":
148
+ need_code: bool | None = self._handle_mfa()
149
+ if self.login_json["error"]:
135
150
  raise LoginResponseError(self.login_json["error"])
136
151
  if need_code:
137
152
  return True
@@ -140,144 +155,183 @@ class FTSession:
140
155
  self._save_cookies()
141
156
  return False
142
157
 
143
- def login_two(self, code):
144
- """Method to finish login to the Firstrade platform."""
145
- data = {
158
+ def login_two(self, code: str) -> None:
159
+ """Finish login to the Firstrade platform."""
160
+ data: dict[str, str | None] = {
146
161
  "otpCode": code,
147
162
  "verificationSid": self.session.headers["sid"],
148
163
  "remember_for": "30",
149
164
  "t_token": self.t_token,
150
165
  }
151
- response = self.session.post(urls.verify_pin(), data=data)
152
- self.login_json = response.json()
153
- if self.login_json["error"] != "":
166
+ response: requests.Response = self._request("post", urls.verify_pin(), data=data)
167
+ self.login_json: dict[str, str] = response.json()
168
+ if not self.login_json["error"]:
154
169
  raise LoginResponseError(self.login_json["error"])
155
170
  self.session.headers["ftat"] = self.login_json["ftat"]
156
171
  self.session.headers["sid"] = self.login_json["sid"]
157
172
  self._save_cookies()
158
173
 
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)
174
+ def delete_cookies(self) -> None:
175
+ """Delete the session cookies."""
176
+ path: Path = Path(self.profile_path) / f"ft_cookies{self.username}.json" if self.profile_path is not None else Path(f"ft_cookies{self.username}.json")
177
+ path.unlink()
166
178
 
167
- def _load_cookies(self):
168
- """
169
- Checks if session cookies were saved.
179
+ def _load_cookies(self) -> str:
180
+ """Check if session cookies were saved.
170
181
 
171
182
  Returns:
172
183
  str: The saved session token.
173
- """
174
184
 
185
+ """
175
186
  ftat = ""
176
- directory = (
177
- os.path.abspath(self.profile_path) if self.profile_path is not None else "."
178
- )
179
- if not os.path.exists(directory):
180
- os.makedirs(directory)
181
-
182
- for filename in os.listdir(directory):
183
- if filename.endswith(f"{self.username}.pkl"):
184
- filepath = os.path.join(directory, filename)
185
- with open(filepath, "rb") as f:
186
- ftat = pickle.load(f)
187
+ directory: Path = Path(self.profile_path) if self.profile_path is not None else Path()
188
+ if not directory.exists():
189
+ directory.mkdir(parents=True)
190
+
191
+ for filepath in directory.iterdir():
192
+ if filepath.name.endswith(f"{self.username}.json"):
193
+ with filepath.open(mode="r") as f:
194
+ ftat: str = json.load(fp=f)
187
195
  return ftat
188
196
 
189
- def _save_cookies(self):
190
- """Saves session cookies to a file."""
197
+ def _save_cookies(self) -> str | None:
198
+ """Save session cookies to a file."""
191
199
  if self.profile_path is not None:
192
- directory = os.path.abspath(self.profile_path)
193
- if not os.path.exists(directory):
194
- os.makedirs(directory)
195
- path = os.path.join(self.profile_path, f"ft_cookies{self.username}.pkl")
200
+ directory = Path(self.profile_path)
201
+ if not directory.exists():
202
+ directory.mkdir(parents=True)
203
+ path: Path = directory / f"ft_cookies{self.username}.json"
196
204
  else:
197
- path = f"ft_cookies{self.username}.pkl"
198
- with open(path, "wb") as f:
199
- ftat = self.session.headers.get("ftat")
200
- pickle.dump(ftat, f)
205
+ path = Path(f"ft_cookies{self.username}.json")
206
+ with path.open("w") as f:
207
+ ftat: str | None = self.session.headers.get("ftat")
208
+ json.dump(obj=ftat, fp=f)
201
209
 
202
210
  @staticmethod
203
- def _mask_email(email):
204
- """
205
- Masks the email for use in the API.
211
+ def _mask_email(email: str) -> str:
212
+ """Mask the email for use in the API.
206
213
 
207
214
  Args:
208
215
  email (str): The email address to be masked.
209
216
 
210
217
  Returns:
211
218
  str: The masked email address.
219
+
212
220
  """
213
- local, domain = email.split("@")
214
- masked_local = local[0] + "*" * 4
221
+ local, domain = email.split(sep="@")
222
+ masked_local: str = local[0] + "*" * 4
215
223
  domain_name, tld = domain.split(".")
216
- masked_domain = domain_name[0] + "*" * 4
224
+ masked_domain: str = domain_name[0] + "*" * 4
217
225
  return f"{masked_local}@{masked_domain}.{tld}"
218
226
 
219
- def _handle_mfa(self):
220
- """
221
- Handles multi-factor authentication.
227
+ def _handle_mfa(self) -> bool:
228
+ """Handle multi-factor authentication.
222
229
 
223
230
  This method processes the MFA requirements based on the login response and user-provided details.
224
231
 
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
232
  """
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
- break
248
- elif item["channel"] == "email" and self.email is not None:
249
- if self.email == item["recipientMask"]:
250
- data = {
251
- "recipientId": item["recipientId"],
252
- "t_token": self.t_token,
253
- }
254
- break
255
- response = self.session.post(urls.request_code(), data=data)
256
- elif self.login_json["mfa"] and self.mfa_secret is not None:
257
- mfa_otp = pyotp.TOTP(self.mfa_secret).now()
258
- data = {
259
- "mfaCode": mfa_otp,
260
- "remember_for": "30",
261
- "t_token": self.t_token,
262
- }
263
- response = self.session.post(urls.verify_pin(), data=data)
233
+ response: requests.Response | None = None
234
+ data: dict[str, str | None] = {}
235
+
236
+ if self.pin:
237
+ response: requests.Response = self._handle_pin_mfa(data)
238
+ elif (self.email or self.phone) and not self.mfa_secret:
239
+ response: requests.Response = self._handle_otp_mfa(data)
240
+ elif self.mfa_secret:
241
+ response: requests.Response = self._handle_secret_mfa(data)
242
+ else:
243
+ error_msg = "MFA required but no valid MFA method was provided (pin, email/phone, or mfa_secret)."
244
+ raise LoginError(error_msg)
245
+
264
246
  self.login_json = response.json()
265
- if self.login_json["error"] == "":
266
- if self.pin or self.mfa_secret is not None:
267
- self.session.headers["sid"] = self.login_json["sid"]
268
- return False
269
- self.session.headers["sid"] = self.login_json["verificationSid"]
270
- return True
247
+ if self.login_json["error"]:
248
+ raise LoginResponseError(self.login_json["error"])
271
249
 
272
- def __getattr__(self, name):
273
- """
274
- Forwards unknown attribute access to session object.
250
+ if self.pin or self.mfa_secret:
251
+ self.session.headers["sid"] = self.login_json["sid"]
252
+ return False
253
+ self.session.headers["sid"] = self.login_json["verificationSid"]
254
+ return True
255
+
256
+ def _handle_pin_mfa(self, data: dict[str, str | None]) -> requests.Response:
257
+ """Handle PIN-based MFA."""
258
+ data.update({
259
+ "pin": self.pin,
260
+ "remember_for": "30",
261
+ "t_token": self.t_token,
262
+ })
263
+ return self._request("post", urls.verify_pin(), data=data)
264
+
265
+ def _handle_otp_mfa(self, data: dict[str, str | None]) -> requests.Response:
266
+ """Handle email/phone OTP-based MFA."""
267
+ if not self.otp_options:
268
+ error_msg = "No OTP options available."
269
+ raise LoginResponseError(error_msg)
270
+
271
+ for item in self.otp_options:
272
+ if (item["channel"] == "sms" and self.phone and self.phone in item["recipientMask"]) or (item["channel"] == "email" and self.email and self.email == item["recipientMask"]):
273
+ data.update({
274
+ "recipientId": item["recipientId"],
275
+ "t_token": self.t_token,
276
+ })
277
+ break
278
+
279
+ return self._request("post", urls.request_code(), data=data)
280
+
281
+ def _handle_secret_mfa(self, data: dict[str, str | None]) -> requests.Response:
282
+ """Handle MFA secret-based authentication."""
283
+ mfa_otp = pyotp.TOTP(self.mfa_secret).now()
284
+ data.update({
285
+ "mfaCode": mfa_otp,
286
+ "remember_for": "30",
287
+ "t_token": self.t_token,
288
+ })
289
+ return self._request("post", urls.verify_pin(), data=data)
290
+
291
+ def _request(self, method, url, **kwargs):
292
+ """Send HTTP request and log the full response content if debug=True."""
293
+ resp = self.session.request(method, url, **kwargs)
294
+
295
+ if self.debug:
296
+ # Suppress urllib3 / http.client debug so we only see this log
297
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
298
+
299
+ # Basic request info
300
+ logger.debug(f">>> {method.upper()} {url}")
301
+ logger.debug(f"<<< Status: {resp.status_code}")
302
+ logger.debug(f"<<< Headers: {resp.headers}")
303
+
304
+ # Log raw bytes length
305
+ try:
306
+ logger.debug(f"<<< Raw bytes length: {len(resp.content)}")
307
+ except Exception as e:
308
+ logger.debug(f"<<< Could not read raw bytes: {e}")
309
+
310
+ # Log pretty JSON (if any)
311
+ try:
312
+ import json as pyjson
313
+ # This automatically uses requests decompression if gzip is set
314
+ json_body = resp.json()
315
+ pretty = pyjson.dumps(json_body, indent=2)
316
+ logger.debug(f"<<< JSON body:\n{pretty}")
317
+ except Exception as e:
318
+ # If JSON decoding fails, fallback to raw text
319
+ try:
320
+ logger.debug(f"<<< Body (text):\n{resp.text}")
321
+ except Exception as e2:
322
+ logger.debug(f"<<< Could not read body text: {e2}")
323
+
324
+ return resp
325
+
326
+ def __getattr__(self, name: str) -> object:
327
+ """Forward unknown attribute access to session object.
275
328
 
276
329
  Args:
277
330
  name (str): The name of the attribute to be accessed.
278
331
 
279
332
  Returns:
280
333
  The value of the requested attribute from the session object.
334
+
281
335
  """
282
336
  return getattr(self.session, name)
283
337
 
@@ -285,21 +339,20 @@ class FTSession:
285
339
  class FTAccountData:
286
340
  """Dataclass for storing account information."""
287
341
 
288
- def __init__(self, session):
289
- """
290
- Initializes a new instance of the FTAccountData class.
342
+ def __init__(self, session: requests.Session) -> None:
343
+ """Initialize a new instance of the FTAccountData class.
291
344
 
292
345
  Args:
293
- session (requests.Session):
294
- The session object used for making HTTP requests.
346
+ session (requests.Session): The session object used for making HTTP requests.
347
+
295
348
  """
296
- self.session = session
297
- self.all_accounts = []
298
- self.account_numbers = []
299
- self.account_balances = {}
300
- response = self.session.get(url=urls.user_info())
301
- self.user_info = response.json()
302
- response = self.session.get(urls.account_list())
349
+ self.session: requests.Session = session
350
+ self.all_accounts: list[dict[str, object]] = []
351
+ self.account_numbers: list[str] = []
352
+ self.account_balances: dict[str, object] = {}
353
+ response: requests.Response = self.session._request("get", url=urls.user_info())
354
+ self.user_info: dict[str, object] = response.json()
355
+ response: requests.Response = self.session._request("get", urls.account_list())
303
356
  if response.status_code != 200 or response.json()["error"] != "":
304
357
  raise AccountResponseError(response.json()["error"])
305
358
  self.all_accounts = response.json()
@@ -307,90 +360,93 @@ class FTAccountData:
307
360
  self.account_numbers.append(item["account"])
308
361
  self.account_balances[item["account"]] = item["total_value"]
309
362
 
310
- def get_account_balances(self, account):
311
- """Gets account balances for a given account.
363
+ def get_account_balances(self, account: str) -> dict[str, object]:
364
+ """Get account balances for a given account.
312
365
 
313
366
  Args:
314
367
  account (str): Account number of the account you want to get balances for.
315
368
 
316
369
  Returns:
317
370
  dict: Dict of the response from the API.
371
+
318
372
  """
319
- response = self.session.get(urls.account_balances(account))
373
+ response: requests.Response = self.session._request("get", urls.account_balances(account))
320
374
  return response.json()
321
375
 
322
- def get_positions(self, account):
323
- """Gets currently held positions for a given account.
376
+ def get_positions(self, account: str) -> dict[str, object]:
377
+ """Get currently held positions for a given account.
324
378
 
325
379
  Args:
326
380
  account (str): Account number of the account you want to get positions for.
327
381
 
328
382
  Returns:
329
383
  dict: Dict of the response from the API.
330
- """
331
384
 
332
- response = self.session.get(urls.account_positions(account))
385
+ """
386
+ response = self.session._request("get", urls.account_positions(account))
333
387
  return response.json()
334
388
 
335
- def get_account_history(self, account, date_range="ytd", custom_range=None):
336
- """Gets account history for a given account.
389
+ def get_account_history(
390
+ self,
391
+ account: str,
392
+ date_range: str = "ytd",
393
+ custom_range: list[str] | None = None,
394
+ ) -> dict[str, object]:
395
+ """Get account history for a given account.
337
396
 
338
397
  Args:
339
398
  account (str): Account number of the account you want to get history for.
340
- range (str): The range of the history. Defaults to "ytd".
399
+ date_range (str): The range of the history. Defaults to "ytd".
341
400
  Available options are
342
401
  ["today", "1w", "1m", "2m", "mtd", "ytd", "ly", "cust"].
343
- custom_range (str): The custom range of the history.
402
+ custom_range (list[str] | None): The custom range of the history.
344
403
  Defaults to None. If range is "cust",
345
404
  this parameter is required.
346
405
  Format: ["YYYY-MM-DD", "YYYY-MM-DD"].
347
406
 
348
407
  Returns:
349
408
  dict: Dict of the response from the API.
409
+
350
410
  """
351
411
  if date_range == "cust" and custom_range is None:
352
- raise ValueError("Custom range is required when date_range is 'cust'.")
353
- response = self.session.get(
354
- urls.account_history(account, date_range, custom_range)
412
+ raise ValueError("Custom range required.")
413
+ response: requests.Response = self.session._request(
414
+ "get", urls.account_history(account, date_range, custom_range),
355
415
  )
356
416
  return response.json()
357
417
 
358
- def get_orders(self, account):
359
- """
360
- Retrieves existing order data for a given account.
418
+ def get_orders(self, account: str) -> list[dict[str, object]]:
419
+ """Retrieve existing order data for a given account.
361
420
 
362
421
  Args:
363
- ft_session (FTSession): The session object used for making HTTP requests to Firstrade.
364
422
  account (str): Account number of the account to retrieve orders for.
365
423
 
366
424
  Returns:
367
425
  list: A list of dictionaries, each containing details about an order.
368
- """
369
426
 
370
- response = self.session.get(url=urls.order_list(account))
427
+ """
428
+ response = self.session._request("get", url=urls.order_list(account))
371
429
  return response.json()
372
430
 
373
- def cancel_order(self, order_id):
374
- """
375
- Cancels an existing order.
431
+ def cancel_order(self, order_id: str) -> dict[str, object]:
432
+ """Cancel an existing order.
376
433
 
377
434
  Args:
378
435
  order_id (str): The order ID to cancel.
379
436
 
380
437
  Returns:
381
438
  dict: A dictionary containing the response data.
382
- """
383
439
 
440
+ """
384
441
  data = {
385
442
  "order_id": order_id,
386
443
  }
387
444
 
388
- response = self.session.post(url=urls.cancel_order(), data=data)
445
+ response = self.session._request("post", url=urls.cancel_order(), data=data)
389
446
  return response.json()
390
447
 
391
- def get_balance_overview(self, account, keywords=None):
392
- """
393
- Returns a filtered, flattened view of useful balance fields.
448
+ def get_balance_overview(self, account: str, keywords: list[str] | None = None) -> dict[str, object]:
449
+ """Return a filtered, flattened view of useful balance fields.
394
450
 
395
451
  This is a convenience helper over `get_account_balances` to quickly
396
452
  surface likely relevant numbers such as cash, available cash, and
@@ -404,6 +460,7 @@ class FTAccountData:
404
460
  Returns:
405
461
  dict: A dict mapping dot-notated keys to values from the balances
406
462
  response where the key path contains any of the keywords.
463
+
407
464
  """
408
465
  if keywords is None:
409
466
  keywords = [
@@ -417,22 +474,22 @@ class FTAccountData:
417
474
  "margin",
418
475
  ]
419
476
 
420
- payload = self.get_account_balances(account)
477
+ payload: dict[str, object] = self.get_account_balances(account)
421
478
 
422
- filtered = {}
479
+ filtered: dict[str, object] = {}
423
480
 
424
- def _walk(node, path):
481
+ def _walk(node: object, path: list[str]) -> None:
425
482
  if isinstance(node, dict):
426
483
  for k, v in node.items():
427
- _walk(v, path + [str(k)])
484
+ _walk(node=v, path=[*path, str(object=k)])
428
485
  elif isinstance(node, list):
429
- for i, v in enumerate(node):
430
- _walk(v, path + [str(i)])
486
+ for i, v in enumerate(iterable=node):
487
+ _walk(node=v, path=[*path, str(object=i)])
431
488
  else:
432
- key_path = ".".join(path)
433
- low = key_path.lower()
489
+ key_path: str = ".".join(path)
490
+ low: str = key_path.lower()
434
491
  if any(sub in low for sub in keywords):
435
492
  filtered[key_path] = node
436
493
 
437
- _walk(payload, [])
494
+ _walk(node=payload, path=[])
438
495
  return filtered
firstrade/exceptions.py CHANGED
@@ -36,10 +36,9 @@ class LoginRequestError(LoginError):
36
36
  class LoginResponseError(LoginError):
37
37
  """Exception raised for errors in the API response during login."""
38
38
 
39
- def __init__(self, error_message):
40
- self.message = (
41
- f"Failed to login. API returned the following error: {error_message}"
42
- )
39
+ def __init__(self, error_message: str) -> None:
40
+ """Raise error for login response issues."""
41
+ self.message = f"Failed to login. API returned the following error: {error_message}"
43
42
  super().__init__(self.message)
44
43
 
45
44
 
firstrade/order.py CHANGED
@@ -1,12 +1,11 @@
1
- from enum import Enum
1
+ import enum
2
2
 
3
3
  from firstrade import urls
4
4
  from firstrade.account import FTSession
5
5
 
6
6
 
7
- class PriceType(str, Enum):
8
- """
9
- Enum for valid price types in an order.
7
+ class PriceType(enum.StrEnum):
8
+ """Enum for valid price types in an order.
10
9
 
11
10
  Attributes:
12
11
  MARKET (str): Market order, executed at the current market price.
@@ -15,6 +14,7 @@ class PriceType(str, Enum):
15
14
  STOP_LIMIT (str): Stop-limit order, becomes a limit order once a specified price is reached.
16
15
  TRAILING_STOP_DOLLAR (str): Trailing stop order with a specified dollar amount.
17
16
  TRAILING_STOP_PERCENT (str): Trailing stop order with a specified percentage.
17
+
18
18
  """
19
19
 
20
20
  LIMIT = "2"
@@ -25,9 +25,8 @@ class PriceType(str, Enum):
25
25
  TRAILING_STOP_PERCENT = "6"
26
26
 
27
27
 
28
- class Duration(str, Enum):
29
- """
30
- Enum for valid order durations.
28
+ class Duration(enum.StrEnum):
29
+ """Enum for valid order durations.
31
30
 
32
31
  Attributes:
33
32
  DAY (str): Day order.
@@ -35,6 +34,7 @@ class Duration(str, Enum):
35
34
  PRE_MARKET (str): Pre-market order.
36
35
  AFTER_MARKET (str): After-market order.
37
36
  DAY_EXT (str): Day extended order.
37
+
38
38
  """
39
39
 
40
40
  DAY = "0"
@@ -44,9 +44,8 @@ class Duration(str, Enum):
44
44
  DAY_EXT = "D"
45
45
 
46
46
 
47
- class OrderType(str, Enum):
48
- """
49
- Enum for valid order types.
47
+ class OrderType(enum.StrEnum):
48
+ """Enum for valid order types.
50
49
 
51
50
  Attributes:
52
51
  BUY (str): Buy order.
@@ -55,6 +54,7 @@ class OrderType(str, Enum):
55
54
  BUY_TO_COVER (str): Buy to cover order.
56
55
  BUY_OPTION (str): Buy option order.
57
56
  SELL_OPTION (str): Sell option order.
57
+
58
58
  """
59
59
 
60
60
  BUY = "B"
@@ -65,28 +65,30 @@ class OrderType(str, Enum):
65
65
  SELL_OPTION = "SO"
66
66
 
67
67
 
68
- class OrderInstructions(str, Enum):
69
- """
70
- Enum for valid order instructions.
68
+ class OrderInstructions(enum.StrEnum):
69
+ """Enum for valid order instructions.
71
70
 
72
71
  Attributes:
72
+ NONE (str): No special instruction.
73
73
  AON (str): All or none.
74
74
  OPG (str): At the Open.
75
75
  CLO (str): At the Close.
76
+
76
77
  """
77
78
 
79
+ NONE = "0"
78
80
  AON = "1"
79
81
  OPG = "4"
80
82
  CLO = "5"
81
83
 
82
84
 
83
- class OptionType(str, Enum):
84
- """
85
- Enum for valid option types.
85
+ class OptionType(enum.StrEnum):
86
+ """Enum for valid option types.
86
87
 
87
88
  Attributes:
88
89
  CALL (str): Call option.
89
90
  PUT (str): Put option.
91
+
90
92
  """
91
93
 
92
94
  CALL = "C"
@@ -94,15 +96,16 @@ class OptionType(str, Enum):
94
96
 
95
97
 
96
98
  class Order:
97
- """
98
- Represents an order with methods to place it.
99
+ """Represents an order with methods to place it.
99
100
 
100
101
  Attributes:
101
102
  ft_session (FTSession): The session object for placing orders.
103
+
102
104
  """
103
105
 
104
- def __init__(self, ft_session: FTSession):
105
- self.ft_session = ft_session
106
+ def __init__(self, ft_session: FTSession) -> None:
107
+ """Initialize the Order with a FirstTrade session."""
108
+ self.ft_session: FTSession = ft_session
106
109
 
107
110
  def place_order(
108
111
  self,
@@ -113,13 +116,13 @@ class Order:
113
116
  duration: Duration,
114
117
  quantity: int = 0,
115
118
  price: float = 0.00,
116
- stop_price: float = None,
119
+ stop_price: float | None = None,
120
+ *,
117
121
  dry_run: bool = True,
118
122
  notional: bool = False,
119
- order_instruction: OrderInstructions = "0",
123
+ order_instruction: OrderInstructions = OrderInstructions.NONE,
120
124
  ):
121
- """
122
- Builds and places an order.
125
+ """Build and place an order.
123
126
 
124
127
  Args:
125
128
  account (str): The account number to place the order in.
@@ -134,15 +137,10 @@ class Order:
134
137
  notional (bool, optional): If True, the order will be placed based on a notional dollar amount rather than share quantity. Defaults to False.
135
138
  order_instruction (OrderInstructions, optional): Additional order instructions (e.g., AON, OPG). Defaults to "0".
136
139
 
137
- Raises:
138
- ValueError: If AON orders are not limit orders or if AON orders have a quantity of 100 shares or less.
139
- PreviewOrderError: If the order preview fails.
140
- PlaceOrderError: If the order placement fails.
141
-
142
140
  Returns:
143
141
  dict: A dictionary containing the order confirmation data.
144
- """
145
142
 
143
+ """
146
144
  if price_type == PriceType.MARKET and not notional:
147
145
  price = ""
148
146
  if order_instruction == OrderInstructions.AON and price_type != PriceType.LIMIT:
@@ -164,11 +162,11 @@ class Order:
164
162
  if notional:
165
163
  data["dollar_amount"] = price
166
164
  del data["shares"]
167
- if price_type in [PriceType.LIMIT, PriceType.STOP_LIMIT]:
165
+ if price_type in {PriceType.LIMIT, PriceType.STOP_LIMIT}:
168
166
  data["limit_price"] = price
169
- if price_type in [PriceType.STOP, PriceType.STOP_LIMIT]:
167
+ if price_type in {PriceType.STOP, PriceType.STOP_LIMIT}:
170
168
  data["stop_price"] = stop_price
171
- response = self.ft_session.post(url=urls.order(), data=data)
169
+ response: requests.Response = self.ft_session._request("post", url=urls.order(), data=data)
172
170
  if response.status_code != 200 or response.json()["error"] != "":
173
171
  return response.json()
174
172
  preview_data = response.json()
@@ -176,7 +174,7 @@ class Order:
176
174
  return preview_data
177
175
  data["preview"] = "false"
178
176
  data["stage"] = "P"
179
- response = self.ft_session.post(url=urls.order(), data=data)
177
+ response = self.ft_session._request("post", url=urls.order(), data=data)
180
178
  return response.json()
181
179
 
182
180
  def place_option_order(
@@ -187,13 +185,13 @@ class Order:
187
185
  order_type: OrderType,
188
186
  contracts: int,
189
187
  duration: Duration,
190
- stop_price: float = None,
188
+ stop_price: float | None = None,
191
189
  price: float = 0.00,
190
+ *,
192
191
  dry_run: bool = True,
193
- order_instruction: OrderInstructions = "0",
192
+ order_instruction: OrderInstructions = OrderInstructions.NONE,
194
193
  ):
195
- """
196
- Builds and places an option order.
194
+ """Build and place an option order.
197
195
 
198
196
  Args:
199
197
  account (str): The account number to place the order in.
@@ -209,13 +207,11 @@ class Order:
209
207
 
210
208
  Raises:
211
209
  ValueError: If AON orders are not limit orders or if AON orders have a quantity of 100 contracts or less.
212
- PreviewOrderError: If there is an error during the preview of the order.
213
- PlaceOrderError: If there is an error during the placement of the order.
214
210
 
215
211
  Returns:
216
212
  dict: A dictionary containing the order confirmation data.
217
- """
218
213
 
214
+ """
219
215
  if order_instruction == OrderInstructions.AON and price_type != PriceType.LIMIT:
220
216
  raise ValueError("AON orders must be a limit order.")
221
217
  if order_instruction == OrderInstructions.AON and contracts <= 100:
@@ -231,16 +227,16 @@ class Order:
231
227
  "account": account,
232
228
  "price_type": price_type,
233
229
  }
234
- if price_type in [PriceType.LIMIT, PriceType.STOP_LIMIT]:
230
+ if price_type in {PriceType.LIMIT, PriceType.STOP_LIMIT}:
235
231
  data["limit_price"] = price
236
- if price_type in [PriceType.STOP, PriceType.STOP_LIMIT]:
232
+ if price_type in {PriceType.STOP, PriceType.STOP_LIMIT}:
237
233
  data["stop_price"] = stop_price
238
234
 
239
- response = self.ft_session.post(url=urls.option_order(), data=data)
235
+ response = self.ft_session._request("post", url=urls.option_order(), data=data)
240
236
  if response.status_code != 200 or response.json()["error"] != "":
241
237
  return response.json()
242
238
  if dry_run:
243
239
  return response.json()
244
240
  data["preview"] = "false"
245
- response = self.ft_session.post(url=urls.option_order(), data=data)
241
+ response = self.ft_session._request("post", url=urls.option_order(), data=data)
246
242
  return response.json()
firstrade/symbols.py CHANGED
@@ -1,11 +1,12 @@
1
+ from typing import Any
2
+
1
3
  from firstrade import urls
2
4
  from firstrade.account import FTSession
3
5
  from firstrade.exceptions import QuoteRequestError, QuoteResponseError
4
6
 
5
7
 
6
8
  class SymbolQuote:
7
- """
8
- Data class representing a stock quote for a given symbol.
9
+ """Data class representing a stock quote for a given symbol.
9
10
 
10
11
  Attributes:
11
12
  ft_session (FTSession): The session object used for making HTTP requests to Firstrade.
@@ -38,11 +39,11 @@ class SymbolQuote:
38
39
  realtime (str): Indicates if the quote is real-time.
39
40
  nls (str): Nasdaq last sale.
40
41
  shares (int): The number of shares.
42
+
41
43
  """
42
44
 
43
45
  def __init__(self, ft_session: FTSession, account: str, symbol: str):
44
- """
45
- Initializes a new instance of the SymbolQuote class.
46
+ """Initialize a new instance of the SymbolQuote class.
46
47
 
47
48
  Args:
48
49
  ft_session (FTSession): The session object used for making HTTP requests to Firstrade.
@@ -52,70 +53,70 @@ class SymbolQuote:
52
53
  Raises:
53
54
  QuoteRequestError: If the quote request fails with a non-200 status code.
54
55
  QuoteResponseError: If the quote response contains an error message.
56
+
55
57
  """
56
- self.ft_session = ft_session
57
- response = self.ft_session.get(url=urls.quote(account, symbol))
58
+ self.ft_session: FTSession = ft_session
59
+ response = self.ft_session._request("get", url=urls.quote(account, symbol))
58
60
  if response.status_code != 200:
59
61
  raise QuoteRequestError(response.status_code)
60
- if response.json().get("error", "") != "":
62
+ if response.json().get("error", ""):
61
63
  raise QuoteResponseError(symbol, response.json()["error"])
62
- self.symbol = response.json()["result"]["symbol"]
63
- self.sec_type = response.json()["result"]["sec_type"]
64
- self.tick = response.json()["result"]["tick"]
65
- self.bid = response.json()["result"]["bid"]
66
- self.bid_size = response.json()["result"]["bid_size"]
67
- self.ask = response.json()["result"]["ask"]
68
- self.ask_size = response.json()["result"]["ask_size"]
69
- self.last = response.json()["result"]["last"]
70
- self.change = response.json()["result"]["change"]
71
- self.high = response.json()["result"]["high"]
72
- self.low = response.json()["result"]["low"]
73
- self.bid_mmid = response.json()["result"]["bid_mmid"]
74
- self.ask_mmid = response.json()["result"]["ask_mmid"]
75
- self.last_mmid = response.json()["result"]["last_mmid"]
76
- self.last_size = response.json()["result"]["last_size"]
77
- self.change_color = response.json()["result"]["change_color"]
78
- self.volume = response.json()["result"]["vol"]
79
- self.today_close = response.json()["result"]["today_close"]
80
- self.open = response.json()["result"]["open"]
81
- self.quote_time = response.json()["result"]["quote_time"]
82
- self.last_trade_time = response.json()["result"]["last_trade_time"]
83
- self.company_name = response.json()["result"]["company_name"]
84
- self.exchange = response.json()["result"]["exchange"]
85
- self.has_option = response.json()["result"]["has_option"]
86
- self.is_etf = bool(response.json()["result"]["is_etf"])
64
+ self.symbol: str = response.json()["result"]["symbol"]
65
+ self.sec_type: str = response.json()["result"]["sec_type"]
66
+ self.tick: str = response.json()["result"]["tick"]
67
+ self.bid: str = response.json()["result"]["bid"]
68
+ self.bid_size: str = response.json()["result"]["bid_size"]
69
+ self.ask: str = response.json()["result"]["ask"]
70
+ self.ask_size: str = response.json()["result"]["ask_size"]
71
+ self.last: str = response.json()["result"]["last"]
72
+ self.change: str = response.json()["result"]["change"]
73
+ self.high: str = response.json()["result"]["high"]
74
+ self.low: str = response.json()["result"]["low"]
75
+ self.bid_mmid: str = response.json()["result"]["bid_mmid"]
76
+ self.ask_mmid: str = response.json()["result"]["ask_mmid"]
77
+ self.last_mmid: str = response.json()["result"]["last_mmid"]
78
+ self.last_size: int = response.json()["result"]["last_size"]
79
+ self.change_color: str = response.json()["result"]["change_color"]
80
+ self.volume: str = response.json()["result"]["vol"]
81
+ self.today_close: float = response.json()["result"]["today_close"]
82
+ self.open: str = response.json()["result"]["open"]
83
+ self.quote_time: str = response.json()["result"]["quote_time"]
84
+ self.last_trade_time: str = response.json()["result"]["last_trade_time"]
85
+ self.company_name: str = response.json()["result"]["company_name"]
86
+ self.exchange: str = response.json()["result"]["exchange"]
87
+ self.has_option: str = response.json()["result"]["has_option"]
88
+ self.is_etf: bool = bool(response.json()["result"]["is_etf"])
87
89
  self.is_fractional = bool(response.json()["result"]["is_fractional"])
88
- self.realtime = response.json()["result"]["realtime"]
89
- self.nls = response.json()["result"]["nls"]
90
- self.shares = response.json()["result"]["shares"]
90
+ self.realtime: str = response.json()["result"]["realtime"]
91
+ self.nls: str = response.json()["result"]["nls"]
92
+ self.shares: str = response.json()["result"]["shares"]
91
93
 
92
94
 
93
95
  class OptionQuote:
94
- """
95
- Data class representing an option quote for a given symbol.
96
+ """Data class representing an option quote for a given symbol.
96
97
 
97
98
  Attributes:
98
99
  ft_session (FTSession): The session object used for making HTTP requests to Firstrade.
99
100
  symbol (str): The symbol for which the option quote information is retrieved.
100
101
  option_dates (dict): A dict of expiration dates for options on the given symbol.
102
+
101
103
  """
102
104
 
103
105
  def __init__(self, ft_session: FTSession, symbol: str):
104
- """
105
- Initializes a new instance of the OptionQuote class.
106
+ """Initialize a new instance of the OptionQuote class.
106
107
 
107
108
  Args:
108
109
  ft_session (FTSession):
109
110
  The session object used for making HTTP requests to Firstrade.
110
111
  symbol (str): The symbol for which the option quote information is retrieved.
112
+
111
113
  """
112
114
  self.ft_session = ft_session
113
115
  self.symbol = symbol
114
116
  self.option_dates = self.get_option_dates(symbol)
115
117
 
116
118
  def get_option_dates(self, symbol: str):
117
- """
118
- Retrieves the expiration dates for options on a given symbol.
119
+ """Retrieve the expiration dates for options on a given symbol.
119
120
 
120
121
  Args:
121
122
  symbol (str): The symbol for which the expiration dates are retrieved.
@@ -123,16 +124,12 @@ class OptionQuote:
123
124
  Returns:
124
125
  dict: A dict of expiration dates and other information for options on the given symbol.
125
126
 
126
- Raises:
127
- QuoteRequestError: If the request for option dates fails with a non-200 status code.
128
- QuoteResponseError: If the response for option dates contains an error message.
129
127
  """
130
- response = self.ft_session.get(url=urls.option_dates(symbol))
128
+ response = self.ft_session._request("get", url=urls.option_dates(symbol))
131
129
  return response.json()
132
130
 
133
- def get_option_quote(self, symbol: str, date: str):
134
- """
135
- Retrieves the quote for a given option symbol.
131
+ def get_option_quote(self, symbol: str, date: str) -> dict[Any, Any]:
132
+ """Retrieve the quote for a given option symbol.
136
133
 
137
134
  Args:
138
135
  symbol (str): The symbol for which the quote is retrieved.
@@ -140,16 +137,12 @@ class OptionQuote:
140
137
  Returns:
141
138
  dict: A dictionary containing the quote and other information for the given option symbol.
142
139
 
143
- Raises:
144
- QuoteRequestError: If the request for the option quote fails with a non-200 status code.
145
- QuoteResponseError: If the response for the option quote contains an error message.
146
140
  """
147
- response = self.ft_session.get(url=urls.option_quotes(symbol, date))
141
+ response = self.ft_session._request("get", url=urls.option_quotes(symbol, date))
148
142
  return response.json()
149
143
 
150
144
  def get_greek_options(self, symbol: str, exp_date: str):
151
- """
152
- Retrieves the greeks for options on a given symbol.
145
+ """Retrieve the greeks for options on a given symbol.
153
146
 
154
147
  Args:
155
148
  symbol (str): The symbol for which the greeks are retrieved.
@@ -158,9 +151,6 @@ class OptionQuote:
158
151
  Returns:
159
152
  dict: A dictionary containing the greeks for the options on the given symbol.
160
153
 
161
- Raises:
162
- QuoteRequestError: If the request for the greeks fails with a non-200 status code.
163
- QuoteResponseError: If the response for the greeks contains an error message.
164
154
  """
165
155
  data = {
166
156
  "type": "chain",
@@ -168,5 +158,5 @@ class OptionQuote:
168
158
  "root_symbol": symbol,
169
159
  "exp_date": exp_date,
170
160
  }
171
- response = self.ft_session.post(url=urls.greek_options(), data=data)
161
+ response = self.ft_session._request("post", url=urls.greek_options(), data=data)
172
162
  return response.json()
firstrade/urls.py CHANGED
@@ -1,73 +1,88 @@
1
- def login():
1
+ def login() -> str:
2
+ """Login URL for FirstTrade API."""
2
3
  return "https://api3x.firstrade.com/sess/login"
3
4
 
4
5
 
5
- def request_code():
6
+ def request_code() -> str:
7
+ """Request PIN/MFA option for FirstTrade API."""
6
8
  return "https://api3x.firstrade.com/sess/request_code"
7
9
 
8
10
 
9
- def verify_pin():
11
+ def verify_pin() -> str:
12
+ """Request PIN/MFA verification for FirstTrade API."""
10
13
  return "https://api3x.firstrade.com/sess/verify_pin"
11
14
 
12
15
 
13
- def user_info():
16
+ def user_info() -> str:
17
+ """Retrieve user information URL for FirstTrade API."""
14
18
  return "https://api3x.firstrade.com/private/userinfo"
15
19
 
16
20
 
17
- def account_list():
21
+ def account_list() -> str:
22
+ """Retrieve account list URL for FirstTrade API."""
18
23
  return "https://api3x.firstrade.com/private/acct_list"
19
24
 
20
25
 
21
- def account_balances(account):
26
+ def account_balances(account: str) -> str:
27
+ """Retrieve account balances URL for FirstTrade API."""
22
28
  return f"https://api3x.firstrade.com/private/balances?account={account}"
23
29
 
24
30
 
25
- def account_positions(account):
26
- return (
27
- f"https://api3x.firstrade.com/private/positions?account={account}&per_page=200"
28
- )
31
+ def account_positions(account: str) -> str:
32
+ """Retrieve account positions URL for FirstTrade API."""
33
+ return f"https://api3x.firstrade.com/private/positions?account={account}&per_page=200"
29
34
 
30
35
 
31
- def quote(account, symbol):
36
+ def quote(account: str, symbol: str) -> str:
37
+ """Symbol quote URL for FirstTrade API."""
32
38
  return f"https://api3x.firstrade.com/public/quote?account={account}&q={symbol}"
33
39
 
34
40
 
35
- def order():
41
+ def order() -> str:
42
+ """Place equity order URL for FirstTrade API."""
36
43
  return "https://api3x.firstrade.com/private/stock_order"
37
44
 
38
45
 
39
- def order_list(account):
46
+ def order_list(account: str) -> str:
47
+ """Retrieve placed order list URL for FirstTrade API."""
40
48
  return f"https://api3x.firstrade.com/private/order_status?account={account}"
41
49
 
42
50
 
43
- def account_history(account, date_range, custom_range):
51
+ def account_history(account: str, date_range: str, custom_range: list[str] | None) -> str:
52
+ """Retrieve account history URL for FirstTrade API."""
44
53
  if custom_range is None:
45
54
  return f"https://api3x.firstrade.com/private/account_history?range={date_range}&page=1&account={account}&per_page=1000"
46
55
  return f"https://api3x.firstrade.com/private/account_history?range={date_range}&range_arr[]={custom_range[0]}&range_arr[]={custom_range[1]}&page=1&account={account}&per_page=1000"
47
56
 
48
57
 
49
- def cancel_order():
58
+ def cancel_order() -> str:
59
+ """Cancel placed order URL for FirstTrade API."""
50
60
  return "https://api3x.firstrade.com/private/cancel_order"
51
61
 
52
62
 
53
- def option_dates(symbol):
63
+ def option_dates(symbol: str) -> str:
64
+ """Option dates URL for FirstTrade API."""
54
65
  return f"https://api3x.firstrade.com/public/oc?m=get_exp_dates&root_symbol={symbol}"
55
66
 
56
67
 
57
- def option_quotes(symbol, date):
68
+ def option_quotes(symbol: str, date: str) -> str:
69
+ """Option quotes URL for FirstTrade API."""
58
70
  return f"https://api3x.firstrade.com/public/oc?m=get_oc&root_symbol={symbol}&exp_date={date}&chains_range=A"
59
71
 
60
72
 
61
- def greek_options():
73
+ def greek_options() -> str:
74
+ """Greek options analytical data URL for FirstTrade API."""
62
75
  return "https://api3x.firstrade.com/private/greekoptions/analytical"
63
76
 
64
77
 
65
- def option_order():
78
+ def option_order() -> str:
79
+ """Place option order URL for FirstTrade API."""
66
80
  return "https://api3x.firstrade.com/private/option_order"
67
81
 
68
82
 
69
- def session_headers():
70
- headers = {
83
+ def session_headers() -> dict[str, str]:
84
+ """Session headers for FirstTrade API."""
85
+ headers: dict[str, str] = {
71
86
  "Accept-Encoding": "gzip",
72
87
  "Connection": "Keep-Alive",
73
88
  "Host": "api3x.firstrade.com",
@@ -76,5 +91,6 @@ def session_headers():
76
91
  return headers
77
92
 
78
93
 
79
- def access_token():
94
+ def access_token() -> str:
95
+ """Access token for FirstTrade API."""
80
96
  return "833w3XuIFycv18ybi"
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firstrade
3
- Version: 0.0.34
3
+ Version: 0.0.37
4
4
  Summary: An unofficial API for Firstrade
5
5
  Home-page: https://github.com/MaxxRK/firstrade-api
6
- Download-URL: https://github.com/MaxxRK/firstrade-api/archive/refs/tags/0034.tar.gz
6
+ Download-URL: https://github.com/MaxxRK/firstrade-api/archive/refs/tags/0037.tar.gz
7
7
  Author: MaxxRK
8
8
  Author-email: maxxrk@pm.me
9
9
  License: MIT
@@ -13,8 +13,6 @@ Classifier: Intended Audience :: Developers
13
13
  Classifier: Topic :: Internet :: WWW/HTTP :: Session
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.8
17
- Classifier: Programming Language :: Python :: 3.9
18
16
  Classifier: Programming Language :: Python :: 3.10
19
17
  Classifier: Programming Language :: Python :: 3.11
20
18
  Classifier: Programming Language :: Python :: 3.12
@@ -22,8 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
22
20
  Description-Content-Type: text/markdown
23
21
  License-File: LICENSE
24
22
  Requires-Dist: requests
25
- Requires-Dist: beautifulsoup4
26
- Requires-Dist: lxml
23
+ Requires-Dist: pyotp
27
24
  Dynamic: author
28
25
  Dynamic: author-email
29
26
  Dynamic: classifier
@@ -0,0 +1,11 @@
1
+ firstrade/__init__.py,sha256=fNiWYgSTjElY1MNv0Ug-sVLMTR2z_Ngri_FY7Pekdrw,95
2
+ firstrade/account.py,sha256=U_-4Kg0veehd1-wo_KHNmScfD_xLFTilFZjIWUlByQM,19270
3
+ firstrade/exceptions.py,sha256=17speAxfarT1U-k_HWvfUanpUk6U2bCZ2ag49MdWbw0,1928
4
+ firstrade/order.py,sha256=K1-mpvu2ooUBxNtXkX_H8mdJMoRiUG1JDqsmpzfdFBQ,8530
5
+ firstrade/symbols.py,sha256=v6ejLCLo3L_JzPRN3B2yMYn-5qBVCRG-3cx6Y7IMrb0,7466
6
+ firstrade/urls.py,sha256=atalvLvevb1CAOBOdUIX3HAebw3TSRuIhCYFhSXIbQ8,3294
7
+ firstrade-0.0.37.dist-info/licenses/LICENSE,sha256=wPEQjDqm5zMBmEcZp219Labmq_YIjhudpZiUzyVKaFA,1057
8
+ firstrade-0.0.37.dist-info/METADATA,sha256=kpZfe0iUMNulPdNO0THdcOeRYFJBA6nKLsSGU7vaTr8,3238
9
+ firstrade-0.0.37.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
+ firstrade-0.0.37.dist-info/top_level.txt,sha256=tdA8v-KDxU1u4VV6soiNWGBlni4ojv_t_j2wFn5nZcs,10
11
+ firstrade-0.0.37.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- firstrade/__init__.py,sha256=fNiWYgSTjElY1MNv0Ug-sVLMTR2z_Ngri_FY7Pekdrw,95
2
- firstrade/account.py,sha256=4tS-m_85RW6E5YM6Srn9Wn-6TJKV0fDZYnMId7Rb5B4,15870
3
- firstrade/exceptions.py,sha256=OrWB83rc33LSxrI7WxXo4o7FcIfmvPSC9bAY8K1pn7U,1886
4
- firstrade/order.py,sha256=_b1SnqagwBu7KUmvzSUcp8iMOC3I3k-QDjiDLhlVk7E,8710
5
- firstrade/symbols.py,sha256=RH36QLx5v3rKPpBidyJFwGJSDkF5u5f2ILTSZNAGXGQ,7903
6
- firstrade/urls.py,sha256=Iw10isyvoqKwiSl3TVuIbos5INZzIEwpln3HcZ7P5aw,2125
7
- firstrade-0.0.34.dist-info/licenses/LICENSE,sha256=wPEQjDqm5zMBmEcZp219Labmq_YIjhudpZiUzyVKaFA,1057
8
- firstrade-0.0.34.dist-info/METADATA,sha256=cEo6oLxI6nGGQGTOM1wAfISYjt-5jZ1B_WirQsalSdc,3367
9
- firstrade-0.0.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- firstrade-0.0.34.dist-info/top_level.txt,sha256=tdA8v-KDxU1u4VV6soiNWGBlni4ojv_t_j2wFn5nZcs,10
11
- firstrade-0.0.34.dist-info/RECORD,,