diff --git a/.env.example b/.env.example index 50e14dee..0d39ef8a 100644 --- a/.env.example +++ b/.env.example @@ -28,7 +28,7 @@ FENNEL= FIDELITY= # Firstrade -# FIRSTRADE=FIRSTRADE_USERNAME:FIRSTRADE_PASSWORD:FIRSTRADE_PIN +# FIRSTRADE=FIRSTRADE_USERNAME:FIRSTRADE_PASSWORD:FIRSTRADE_OTP FIRSTRADE= # Public diff --git a/README.md b/README.md index 7ef81d17..42ce9531 100644 --- a/README.md +++ b/README.md @@ -192,10 +192,18 @@ Made by [MaxxRK](https://github.com/MaxxRK/) using the [firstrade-api](https://g Required `.env` variables: - `FIRSTRADE_USERNAME` - `FIRSTRADE_PASSWORD` -- `FIRSTRADE_PIN` +- `FIRSTRADE_OTP` `.env` file format: -- `FIRSTRADE=FIRSTRADE_USERNAME:FIRSTRADE_PASSWORD:FIRSTRADE_PIN` +- `FIRSTRADE=FIRSTRADE_USERNAME:FIRSTRADE_PASSWORD:FIRSTRADE_OTP` + +- Note: You may now use any firstrade supported 2fa method, including pin, SMS, Email, or an Authenticator. Place any ONE of the below in the `FIRSTRADE_OTP` field: +- Pin: Use the login pin you created when setting up the account. +- Phone: Enter your 10 digit phone number with no spaces. I.E. 1234567890 +- Email: Enter your full email address. I.E. autorsa@autorsa.com +- Authenticator: Use the code generated by Firstrade when adding an authenticator. Click on "Can't scan it?" to get the code. + +If you get errors after upgrading, try clearing your cookies in the `creds` folder and then trying again. ### Public Made by yours truly using using [public-invest-api](https://github.com/NelsonDane/public-invest-api). Consider giving me a ⭐ diff --git a/autoRSA.py b/autoRSA.py index 91d3ea57..9467f6c5 100644 --- a/autoRSA.py +++ b/autoRSA.py @@ -107,7 +107,7 @@ def fun_run(orderObj: stockOrder, command, botObj=None, loop=None): ), broker, ) - elif broker.lower() in ["fennel", "public"]: + elif broker.lower() in ["fennel", "firstrade", "public"]: # Requires bot object and loop orderObj.set_logged_in( globals()[fun_name](botObj=botObj, loop=loop), broker diff --git a/firstradeAPI.py b/firstradeAPI.py index 5e18eab1..5bca0ef2 100644 --- a/firstradeAPI.py +++ b/firstradeAPI.py @@ -1,6 +1,7 @@ # Donald Ryan Gullett(MaxxRK) # Firstrade API +import asyncio import os import pprint import traceback @@ -10,20 +11,17 @@ from firstrade import account as ft_account from firstrade import order, symbols -from helperAPI import Brokerage, maskString, printAndDiscord, printHoldings, stockOrder +from helperAPI import Brokerage, getOTPCodeDiscord, maskString, printAndDiscord, printHoldings, stockOrder -def firstrade_init(FIRSTRADE_EXTERNAL=None): +def firstrade_init(botObj=None, loop=None): # Initialize .env file load_dotenv() - # Import Firstrade account - if not os.getenv("FIRSTRADE") and FIRSTRADE_EXTERNAL is None: + if not os.getenv("FIRSTRADE"): print("Firstrade not found, skipping...") return None accounts = ( os.environ["FIRSTRADE"].strip().split(",") - if FIRSTRADE_EXTERNAL is None - else FIRSTRADE_EXTERNAL.strip().split(",") ) # Log in to Firstrade account print("Logging in to Firstrade...") @@ -36,17 +34,32 @@ def firstrade_init(FIRSTRADE_EXTERNAL=None): firstrade = ft_account.FTSession( username=account[0], password=account[1], - pin=account[2], + pin=account[2] if len(account[2]) == 4 and account[2].isdigit() else None, + phone=account[2][-4:] if len(account[2]) == 10 and account[2].isdigit() else None, + email=account[2] if "@" in account[2] else None, + mfa_secret=account[2] if len(account[2]) > 14 and "@" not in account[2] else None, profile_path="./creds/", ) - account_info = ft_account.FTAccountData(firstrade) + need_code = firstrade.login() + if need_code: + if botObj is None and loop is None: + firstrade.login_two(input("Enter code: ")) + else: + sms_code = asyncio.run_coroutine_threadsafe( + getOTPCodeDiscord(botObj, name, timeout=300, loop=loop), loop + ).result() + if sms_code is None: + raise Exception( + f"Firstrade {index} code not received in time...", loop + ) + firstrade.login_two(sms_code) print("Logged in to Firstrade!") + account_info = ft_account.FTAccountData(firstrade) firstrade_obj.set_logged_in_object(name, firstrade) - for entry in account_info.all_accounts: - account = list(entry.keys())[0] + for account in account_info.account_numbers: firstrade_obj.set_account_number(name, account) firstrade_obj.set_account_totals( - name, account, str(entry[account]["Balance"]) + name, account, account_info.account_balances[account] ) print_accounts = [maskString(a) for a in account_info.account_numbers] print(f"The following Firstrade accounts were found: {print_accounts}") @@ -64,13 +77,14 @@ def firstrade_holdings(firstrade_o: Brokerage, loop=None): obj: ft_account.FTSession = firstrade_o.get_logged_in_objects(key) try: data = ft_account.FTAccountData(obj).get_positions(account=account) - for item in data: - sym = item - if sym == "": - sym = "Unknown" - qty = float(data[item]["quantity"]) - current_price = float(data[item]["price"]) - firstrade_o.set_holdings(key, account, sym, qty, current_price) + for item in data["items"]: + firstrade_o.set_holdings( + key, + account, + item.get("symbol") or "Unknown", + item["quantity"], + item["market_value"], + ) except Exception as e: printAndDiscord(f"{key} {account}: Error getting holdings: {e}", loop) print(traceback.format_exc()) @@ -100,7 +114,7 @@ def firstrade_transaction(firstrade_o: Brokerage, orderObj: stockOrder, loop=Non "Running in DRY mode. No transactions will be made.", loop ) try: - symbol_data = symbols.SymbolQuote(obj, s) + symbol_data = symbols.SymbolQuote(obj, account, s) if symbol_data.last < 1.00: price_type = order.PriceType.LIMIT if orderObj.get_action().capitalize() == "Buy": @@ -115,7 +129,7 @@ def firstrade_transaction(firstrade_o: Brokerage, orderObj: stockOrder, loop=Non else: order_type = order.OrderType.SELL ft_order = order.Order(obj) - ft_order.place_order( + order_conf = ft_order.place_order( account=account, symbol=s, price_type=price_type, @@ -125,20 +139,21 @@ def firstrade_transaction(firstrade_o: Brokerage, orderObj: stockOrder, loop=Non price=price, dry_run=orderObj.get_dry(), ) + print("The order verification produced the following messages: ") - pprint.pprint(ft_order.order_confirmation) + pprint.pprint(order_conf) printAndDiscord( ( f"{key} account {print_account}: The order verification was " + "successful" - if ft_order.order_confirmation["success"] == "Yes" + if order_conf["error"] == "" else "unsuccessful" ), loop, ) - if not ft_order.order_confirmation["success"] == "Yes": + if not order_conf["error"] == "": printAndDiscord( - f"{key} account {print_account}: The order verification produced the following messages: {ft_order.order_confirmation['actiondata']}", + f"{key} account {print_account}: The order verification produced the following messages: {order_conf}", loop, ) except Exception as e: diff --git a/requirements.txt b/requirements.txt index c89dc529..fcd3de9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ asyncio==3.4.3 chaseinvest-api==0.2.3 discord.py==2.4.0 fennel-invest-api==1.1.0 -firstrade==0.0.21 +firstrade==0.0.30 GitPython==3.1.43 public-invest-api==1.0.4 pyotp==2.9.0