firstrade 0.0.35__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,8 +1,8 @@
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
@@ -13,10 +13,10 @@ from firstrade.exceptions import (
13
13
  LoginResponseError,
14
14
  )
15
15
 
16
+ logger = logging.getLogger(__name__)
16
17
 
17
18
  class FTSession:
18
- """
19
- Class creating a session for Firstrade.
19
+ """Class creating a session for Firstrade.
20
20
 
21
21
  This class handles the creation and management of a session for logging into the Firstrade platform.
22
22
  It supports multi-factor authentication (MFA) and can save session cookies for persistent logins.
@@ -24,18 +24,19 @@ class FTSession:
24
24
  Attributes:
25
25
  username (str): Firstrade login username.
26
26
  password (str): Firstrade login password.
27
- pin (str): Firstrade login pin.
27
+ pin (str, optional): Firstrade login pin.
28
28
  email (str, optional): Firstrade MFA email.
29
29
  phone (str, optional): Firstrade MFA phone number.
30
30
  mfa_secret (str, optional): Secret key for generating MFA codes.
31
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.
32
33
  t_token (str, optional): Token used for MFA.
33
34
  otp_options (dict, optional): Options for OTP (One-Time Password) if MFA is enabled.
34
35
  login_json (dict, optional): JSON response from the login request.
35
36
  session (requests.Session): The requests session object used for making HTTP requests.
36
37
 
37
38
  Methods:
38
- __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):
39
40
  Initializes a new instance of the FTSession class.
40
41
  login():
41
42
  Validates and logs into the Firstrade platform.
@@ -51,44 +52,58 @@ class FTSession:
51
52
  Masks the email for use in the API.
52
53
  _handle_mfa():
53
54
  Handles multi-factor authentication.
55
+ _request(method, url, **kwargs):
56
+ HTTP requests wrapper to the API.
57
+
54
58
  """
55
59
 
56
60
  def __init__(
57
61
  self,
58
- username,
59
- password,
60
- pin=None,
61
- email=None,
62
- phone=None,
63
- mfa_secret=None,
64
- profile_path=None,
65
- ):
66
- """
67
- 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.
68
72
 
69
73
  Args:
70
74
  username (str): Firstrade login username.
71
75
  password (str): Firstrade login password.
72
- pin (str): Firstrade login pin.
76
+ pin (str, optional): Firstrade login pin.
73
77
  email (str, optional): Firstrade MFA email.
74
78
  phone (str, optional): Firstrade MFA phone number.
79
+ mfa_secret (str, optional): Firstrade MFA secret key to generate TOTP.
75
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
+
76
83
  """
77
- self.username = username
78
- self.password = password
79
- self.pin = pin
80
- self.email = FTSession._mask_email(email) if email is not None else None
81
- self.phone = phone
82
- self.mfa_secret = mfa_secret
83
- self.profile_path = profile_path
84
- self.t_token = None
85
- self.otp_options = None
86
- 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] = {}
87
103
  self.session = requests.Session()
88
104
 
89
- def login(self):
90
- """
91
- Validates and logs into the Firstrade platform.
105
+ def login(self) -> bool:
106
+ """Validate and log into the Firstrade platform.
92
107
 
93
108
  This method sets up the session headers, loads cookies if available, and performs the login request.
94
109
  It handles multi-factor authentication (MFA) if required.
@@ -96,43 +111,42 @@ class FTSession:
96
111
  Raises:
97
112
  LoginRequestError: If the login request fails with a non-200 status code.
98
113
  LoginResponseError: If the login response contains an error message.
114
+
99
115
  """
100
- self.session.headers = urls.session_headers()
101
- ftat = self._load_cookies()
102
- if ftat != "":
116
+ self.session.headers.update(urls.session_headers())
117
+ ftat: str = self._load_cookies()
118
+ if ftat:
103
119
  self.session.headers["ftat"] = ftat
104
- 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)
105
121
  self.session.headers["access-token"] = urls.access_token()
106
122
 
107
- data = {
123
+ data: dict[str, str] = {
108
124
  "username": r"" + self.username,
109
125
  "password": r"" + self.password,
110
126
  }
111
127
 
112
- response = self.session.post(
128
+ response: requests.Response = self._request(
129
+ "post",
113
130
  url=urls.login(),
114
131
  data=data,
115
132
  )
116
133
  try:
117
- self.login_json = response.json()
118
- except json.decoder.JSONDecodeError:
119
- raise LoginResponseError("Invalid JSON is your account funded?")
120
- if (
121
- "mfa" not in self.login_json
122
- and "ftat" in self.login_json
123
- and self.login_json["error"] == ""
124
- ):
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"]:
125
139
  self.session.headers["sid"] = self.login_json["sid"]
126
140
  return False
127
- self.t_token = self.login_json.get("t_token")
128
- if self.mfa_secret is None:
129
- 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")
130
144
  if response.status_code != 200:
131
145
  raise LoginRequestError(response.status_code)
132
- if self.login_json["error"] != "":
146
+ if self.login_json["error"]:
133
147
  raise LoginResponseError(self.login_json["error"])
134
- need_code = self._handle_mfa()
135
- if self.login_json["error"] != "":
148
+ need_code: bool | None = self._handle_mfa()
149
+ if self.login_json["error"]:
136
150
  raise LoginResponseError(self.login_json["error"])
137
151
  if need_code:
138
152
  return True
@@ -141,149 +155,183 @@ class FTSession:
141
155
  self._save_cookies()
142
156
  return False
143
157
 
144
- def login_two(self, code):
145
- """Method to finish login to the Firstrade platform."""
146
- data = {
158
+ def login_two(self, code: str) -> None:
159
+ """Finish login to the Firstrade platform."""
160
+ data: dict[str, str | None] = {
147
161
  "otpCode": code,
148
162
  "verificationSid": self.session.headers["sid"],
149
163
  "remember_for": "30",
150
164
  "t_token": self.t_token,
151
165
  }
152
- response = self.session.post(urls.verify_pin(), data=data)
153
- self.login_json = response.json()
154
- 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"]:
155
169
  raise LoginResponseError(self.login_json["error"])
156
170
  self.session.headers["ftat"] = self.login_json["ftat"]
157
171
  self.session.headers["sid"] = self.login_json["sid"]
158
172
  self._save_cookies()
159
173
 
160
- def delete_cookies(self):
161
- """Deletes the session cookies."""
162
- if self.profile_path is not None:
163
- path = os.path.join(self.profile_path, f"ft_cookies{self.username}.pkl")
164
- else:
165
- path = f"ft_cookies{self.username}.pkl"
166
- 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()
167
178
 
168
- def _load_cookies(self):
169
- """
170
- Checks if session cookies were saved.
179
+ def _load_cookies(self) -> str:
180
+ """Check if session cookies were saved.
171
181
 
172
182
  Returns:
173
183
  str: The saved session token.
174
- """
175
184
 
185
+ """
176
186
  ftat = ""
177
- directory = (
178
- os.path.abspath(self.profile_path) if self.profile_path is not None else "."
179
- )
180
- if not os.path.exists(directory):
181
- os.makedirs(directory)
182
-
183
- for filename in os.listdir(directory):
184
- if filename.endswith(f"{self.username}.pkl"):
185
- filepath = os.path.join(directory, filename)
186
- with open(filepath, "rb") as f:
187
- 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)
188
195
  return ftat
189
196
 
190
- def _save_cookies(self):
191
- """Saves session cookies to a file."""
197
+ def _save_cookies(self) -> str | None:
198
+ """Save session cookies to a file."""
192
199
  if self.profile_path is not None:
193
- directory = os.path.abspath(self.profile_path)
194
- if not os.path.exists(directory):
195
- os.makedirs(directory)
196
- 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"
197
204
  else:
198
- path = f"ft_cookies{self.username}.pkl"
199
- with open(path, "wb") as f:
200
- ftat = self.session.headers.get("ftat")
201
- 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)
202
209
 
203
210
  @staticmethod
204
- def _mask_email(email):
205
- """
206
- Masks the email for use in the API.
211
+ def _mask_email(email: str) -> str:
212
+ """Mask the email for use in the API.
207
213
 
208
214
  Args:
209
215
  email (str): The email address to be masked.
210
216
 
211
217
  Returns:
212
218
  str: The masked email address.
219
+
213
220
  """
214
- local, domain = email.split("@")
215
- masked_local = local[0] + "*" * 4
221
+ local, domain = email.split(sep="@")
222
+ masked_local: str = local[0] + "*" * 4
216
223
  domain_name, tld = domain.split(".")
217
- masked_domain = domain_name[0] + "*" * 4
224
+ masked_domain: str = domain_name[0] + "*" * 4
218
225
  return f"{masked_local}@{masked_domain}.{tld}"
219
226
 
220
- def _handle_mfa(self):
221
- """
222
- Handles multi-factor authentication.
227
+ def _handle_mfa(self) -> bool:
228
+ """Handle multi-factor authentication.
223
229
 
224
230
  This method processes the MFA requirements based on the login response and user-provided details.
225
231
 
226
- Raises:
227
- LoginRequestError: If the MFA request fails with a non-200 status code.
228
- LoginResponseError: If the MFA response contains an error message.
229
232
  """
230
- if not self.login_json["mfa"] and self.pin is not None:
231
- data = {
232
- "pin": self.pin,
233
- "remember_for": "30",
234
- "t_token": self.t_token,
235
- }
236
- response = self.session.post(urls.verify_pin(), data=data)
237
- self.login_json = response.json()
238
- elif not self.login_json["mfa"] and (
239
- self.email is not None or self.phone is not None
240
- ):
241
- for item in self.otp_options:
242
- if item["channel"] == "sms" and self.phone is not None:
243
- if self.phone in item["recipientMask"]:
244
- data = {
245
- "recipientId": item["recipientId"],
246
- "t_token": self.t_token,
247
- }
248
- break
249
- elif item["channel"] == "email" and self.email is not None:
250
- if self.email == item["recipientMask"]:
251
- data = {
252
- "recipientId": item["recipientId"],
253
- "t_token": self.t_token,
254
- }
255
- break
256
- response = self.session.post(urls.request_code(), data=data)
257
- elif self.login_json["mfa"] and self.mfa_secret is not None:
258
- mfa_otp = pyotp.TOTP(self.mfa_secret).now()
259
- data = {
260
- "mfaCode": mfa_otp,
261
- "remember_for": "30",
262
- "t_token": self.t_token,
263
- }
264
- 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)
265
242
  else:
266
- raise LoginError(
267
- "MFA required but no valid MFA method "
268
- "was provided (pin, email/phone, or mfa_secret)."
269
- )
243
+ error_msg = "MFA required but no valid MFA method was provided (pin, email/phone, or mfa_secret)."
244
+ raise LoginError(error_msg)
245
+
270
246
  self.login_json = response.json()
271
- if self.login_json["error"] == "":
272
- if self.pin or self.mfa_secret is not None:
273
- self.session.headers["sid"] = self.login_json["sid"]
274
- return False
275
- self.session.headers["sid"] = self.login_json["verificationSid"]
276
- return True
247
+ if self.login_json["error"]:
248
+ raise LoginResponseError(self.login_json["error"])
277
249
 
278
- def __getattr__(self, name):
279
- """
280
- 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.
281
328
 
282
329
  Args:
283
330
  name (str): The name of the attribute to be accessed.
284
331
 
285
332
  Returns:
286
333
  The value of the requested attribute from the session object.
334
+
287
335
  """
288
336
  return getattr(self.session, name)
289
337
 
@@ -291,21 +339,20 @@ class FTSession:
291
339
  class FTAccountData:
292
340
  """Dataclass for storing account information."""
293
341
 
294
- def __init__(self, session):
295
- """
296
- 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.
297
344
 
298
345
  Args:
299
- session (requests.Session):
300
- The session object used for making HTTP requests.
346
+ session (requests.Session): The session object used for making HTTP requests.
347
+
301
348
  """
302
- self.session = session
303
- self.all_accounts = []
304
- self.account_numbers = []
305
- self.account_balances = {}
306
- response = self.session.get(url=urls.user_info())
307
- self.user_info = response.json()
308
- 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())
309
356
  if response.status_code != 200 or response.json()["error"] != "":
310
357
  raise AccountResponseError(response.json()["error"])
311
358
  self.all_accounts = response.json()
@@ -313,90 +360,93 @@ class FTAccountData:
313
360
  self.account_numbers.append(item["account"])
314
361
  self.account_balances[item["account"]] = item["total_value"]
315
362
 
316
- def get_account_balances(self, account):
317
- """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.
318
365
 
319
366
  Args:
320
367
  account (str): Account number of the account you want to get balances for.
321
368
 
322
369
  Returns:
323
370
  dict: Dict of the response from the API.
371
+
324
372
  """
325
- response = self.session.get(urls.account_balances(account))
373
+ response: requests.Response = self.session._request("get", urls.account_balances(account))
326
374
  return response.json()
327
375
 
328
- def get_positions(self, account):
329
- """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.
330
378
 
331
379
  Args:
332
380
  account (str): Account number of the account you want to get positions for.
333
381
 
334
382
  Returns:
335
383
  dict: Dict of the response from the API.
336
- """
337
384
 
338
- response = self.session.get(urls.account_positions(account))
385
+ """
386
+ response = self.session._request("get", urls.account_positions(account))
339
387
  return response.json()
340
388
 
341
- def get_account_history(self, account, date_range="ytd", custom_range=None):
342
- """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.
343
396
 
344
397
  Args:
345
398
  account (str): Account number of the account you want to get history for.
346
- range (str): The range of the history. Defaults to "ytd".
399
+ date_range (str): The range of the history. Defaults to "ytd".
347
400
  Available options are
348
401
  ["today", "1w", "1m", "2m", "mtd", "ytd", "ly", "cust"].
349
- custom_range (str): The custom range of the history.
402
+ custom_range (list[str] | None): The custom range of the history.
350
403
  Defaults to None. If range is "cust",
351
404
  this parameter is required.
352
405
  Format: ["YYYY-MM-DD", "YYYY-MM-DD"].
353
406
 
354
407
  Returns:
355
408
  dict: Dict of the response from the API.
409
+
356
410
  """
357
411
  if date_range == "cust" and custom_range is None:
358
- raise ValueError("Custom range is required when date_range is 'cust'.")
359
- response = self.session.get(
360
- 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),
361
415
  )
362
416
  return response.json()
363
417
 
364
- def get_orders(self, account):
365
- """
366
- 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.
367
420
 
368
421
  Args:
369
- ft_session (FTSession): The session object used for making HTTP requests to Firstrade.
370
422
  account (str): Account number of the account to retrieve orders for.
371
423
 
372
424
  Returns:
373
425
  list: A list of dictionaries, each containing details about an order.
374
- """
375
426
 
376
- response = self.session.get(url=urls.order_list(account))
427
+ """
428
+ response = self.session._request("get", url=urls.order_list(account))
377
429
  return response.json()
378
430
 
379
- def cancel_order(self, order_id):
380
- """
381
- Cancels an existing order.
431
+ def cancel_order(self, order_id: str) -> dict[str, object]:
432
+ """Cancel an existing order.
382
433
 
383
434
  Args:
384
435
  order_id (str): The order ID to cancel.
385
436
 
386
437
  Returns:
387
438
  dict: A dictionary containing the response data.
388
- """
389
439
 
440
+ """
390
441
  data = {
391
442
  "order_id": order_id,
392
443
  }
393
444
 
394
- response = self.session.post(url=urls.cancel_order(), data=data)
445
+ response = self.session._request("post", url=urls.cancel_order(), data=data)
395
446
  return response.json()
396
447
 
397
- def get_balance_overview(self, account, keywords=None):
398
- """
399
- 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.
400
450
 
401
451
  This is a convenience helper over `get_account_balances` to quickly
402
452
  surface likely relevant numbers such as cash, available cash, and
@@ -410,6 +460,7 @@ class FTAccountData:
410
460
  Returns:
411
461
  dict: A dict mapping dot-notated keys to values from the balances
412
462
  response where the key path contains any of the keywords.
463
+
413
464
  """
414
465
  if keywords is None:
415
466
  keywords = [
@@ -423,22 +474,22 @@ class FTAccountData:
423
474
  "margin",
424
475
  ]
425
476
 
426
- payload = self.get_account_balances(account)
477
+ payload: dict[str, object] = self.get_account_balances(account)
427
478
 
428
- filtered = {}
479
+ filtered: dict[str, object] = {}
429
480
 
430
- def _walk(node, path):
481
+ def _walk(node: object, path: list[str]) -> None:
431
482
  if isinstance(node, dict):
432
483
  for k, v in node.items():
433
- _walk(v, path + [str(k)])
484
+ _walk(node=v, path=[*path, str(object=k)])
434
485
  elif isinstance(node, list):
435
- for i, v in enumerate(node):
436
- _walk(v, path + [str(i)])
486
+ for i, v in enumerate(iterable=node):
487
+ _walk(node=v, path=[*path, str(object=i)])
437
488
  else:
438
- key_path = ".".join(path)
439
- low = key_path.lower()
489
+ key_path: str = ".".join(path)
490
+ low: str = key_path.lower()
440
491
  if any(sub in low for sub in keywords):
441
492
  filtered[key_path] = node
442
493
 
443
- _walk(payload, [])
494
+ _walk(node=payload, path=[])
444
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.35
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/0035.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
@@ -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,11 +0,0 @@
1
- firstrade/__init__.py,sha256=fNiWYgSTjElY1MNv0Ug-sVLMTR2z_Ngri_FY7Pekdrw,95
2
- firstrade/account.py,sha256=1SMh3fopaHJnyTtHFsbuHXihz7qqv9m8OReDZ3rk-XM,16066
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.35.dist-info/licenses/LICENSE,sha256=wPEQjDqm5zMBmEcZp219Labmq_YIjhudpZiUzyVKaFA,1057
8
- firstrade-0.0.35.dist-info/METADATA,sha256=6nQJDoEBpQBlai_7wzwZiWMqm5r8VWMoL21_2mNdH8M,3238
9
- firstrade-0.0.35.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
- firstrade-0.0.35.dist-info/top_level.txt,sha256=tdA8v-KDxU1u4VV6soiNWGBlni4ojv_t_j2wFn5nZcs,10
11
- firstrade-0.0.35.dist-info/RECORD,,