From b4414c3775222512fc6649655466cb49e344b224 Mon Sep 17 00:00:00 2001 From: maxxrk Date: Thu, 15 Aug 2024 20:11:43 -0500 Subject: [PATCH 01/14] update readme start moving over --- README.md | 5 +++-- firstradeAPI.py | 42 ++++++++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7ef81d17..a3bda451 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ More detailed guides for some of the difficult setups: All brokers: separate account credentials with a colon (":"). For example, `SCHWAB_USERNAME:SCHWAB_PASSWORD`. Separate multiple logins with the same broker with a comma (","). For example, `SCHWAB_USERNAME:SCHWAB_PASSWORD,SCHWAB_USERNAME2:SCHWAB_PASSWORD2`. + Some brokerages require `Playwright` to run. On Windows, the `playwright install` command might not be recognized. If this is the case, run `python -m playwright install` instead. #### Chase @@ -192,10 +193,10 @@ 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` ### Public Made by yours truly using using [public-invest-api](https://github.com/NelsonDane/public-invest-api). Consider giving me a ⭐ diff --git a/firstradeAPI.py b/firstradeAPI.py index 5e18eab1..96691561 100644 --- a/firstradeAPI.py +++ b/firstradeAPI.py @@ -9,6 +9,17 @@ from dotenv import load_dotenv from firstrade import account as ft_account from firstrade import order, symbols +from firstrade.exceptions import ( + PreviewOrderError, + PlaceOrderError, + QuoteRequestError, + QuoteResponseError, + LoginError, + LoginRequestError, + LoginResponseError, + AccountRequestError, + AccountResponseError +) from helperAPI import Brokerage, maskString, printAndDiscord, printHoldings, stockOrder @@ -36,17 +47,23 @@ 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 else None, + phone = account[2][-4:] if len(account[2]) == 10 else None, + email = account[2] if "@" in account[2] else None, + mfa_secret=account[3] if len(account[2]) > 14 else None, profile_path="./creds/", ) - account_info = ft_account.FTAccountData(firstrade) + need_code = firstrade.login() + if need_code: + code = input("Please enter the pin sent to your email/phone: ") + firstrade.login_two(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 +81,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()) From aa6f3a97655391300c87e72ae56e0e0fa0d33396 Mon Sep 17 00:00:00 2001 From: maxxrk Date: Sat, 17 Aug 2024 22:26:46 -0500 Subject: [PATCH 02/14] get orders working --- firstradeAPI.py | 32 +++++++++++--------------------- requirements.txt | 2 +- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/firstradeAPI.py b/firstradeAPI.py index 96691561..d15524a6 100644 --- a/firstradeAPI.py +++ b/firstradeAPI.py @@ -9,17 +9,6 @@ from dotenv import load_dotenv from firstrade import account as ft_account from firstrade import order, symbols -from firstrade.exceptions import ( - PreviewOrderError, - PlaceOrderError, - QuoteRequestError, - QuoteResponseError, - LoginError, - LoginRequestError, - LoginResponseError, - AccountRequestError, - AccountResponseError -) from helperAPI import Brokerage, maskString, printAndDiscord, printHoldings, stockOrder @@ -45,12 +34,12 @@ def firstrade_init(FIRSTRADE_EXTERNAL=None): try: account = account.split(":") firstrade = ft_account.FTSession( - username=account[0], - password=account[1], - pin=account[2] if len(account[2]) == 4 else None, + username = account[0], + password = account[1], + pin = account[2] if len(account[2]) == 4 else None, phone = account[2][-4:] if len(account[2]) == 10 else None, email = account[2] if "@" in account[2] else None, - mfa_secret=account[3] if len(account[2]) > 14 else None, + mfa_secret = account[2] if len(account[2]) > 14 and "@" not in account[2] else None, profile_path="./creds/", ) need_code = firstrade.login() @@ -118,7 +107,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": @@ -133,7 +122,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, @@ -143,20 +132,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 d7fc992d..8a0bd499 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 From e9d2f04076ce69f2e6b855474fcf09e4acb25e08 Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Fri, 16 Aug 2024 12:28:39 -0400 Subject: [PATCH 03/14] add selenium-stealth --- helperAPI.py | 38 +++++++++++++++++++------------------- requirements.txt | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/helperAPI.py b/helperAPI.py index f9ff3ab7..646d46ac 100644 --- a/helperAPI.py +++ b/helperAPI.py @@ -18,8 +18,8 @@ from discord.ext import commands from dotenv import load_dotenv from selenium import webdriver +from selenium_stealth import stealth from selenium.webdriver.chrome.service import Service as ChromiumService -from selenium.webdriver.edge.service import Service as EdgeService # Create task queue task_queue = Queue() @@ -484,32 +484,32 @@ def check_if_page_loaded(driver): def getDriver(DOCKER=False): # Init webdriver options try: + options = webdriver.ChromeOptions() + options.add_argument("start-maximized") + options.add_experimental_option("excludeSwitches", ["enable-automation"]) + options.add_experimental_option('useAutomationExtension', False) + options.add_argument("--disable-blink-features=AutomationControlled") + options.add_argument("--disable-extensions") + options.add_argument("--disable-notifications") + options.add_argument("--headless") if DOCKER: - # Docker uses Chromium - options = webdriver.ChromeOptions() - options.add_argument("--disable-blink-features=AutomationControlled") - options.add_argument("--disable-notifications") + # Special Docker options options.add_argument("--disable-dev-shm-usage") options.add_argument("--no-sandbox") options.add_argument("--disable-gpu") + driver = webdriver.Chrome( + options=options, # Docker uses specific chromedriver installed via apt - driver = webdriver.Chrome( - service=ChromiumService("/usr/bin/chromedriver"), - options=options, - ) - else: - # Otherwise use Edge - options = webdriver.EdgeOptions() - options.add_argument("--disable-blink-features=AutomationControlled") - options.add_argument("--disable-notifications") - driver = webdriver.Edge( - service=EdgeService(), - options=options, - ) + service=ChromiumService("/usr/bin/chromedriver") if DOCKER else None, + ) + stealth( + driver=driver, + platform="Win32", + fix_hairline=True, + ) except Exception as e: print(f"Error getting Driver: {e}") return None - driver.maximize_window() return driver diff --git a/requirements.txt b/requirements.txt index 8a0bd499..fcd3de9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ python-dotenv==1.0.1 requests==2.32.3 robin-stocks==3.1.0 schwab-api==0.4.3 -selenium==4.23.1 +selenium-stealth==1.0.6 setuptools==72.2.0 tastytrade==8.2 vanguard-api==0.2.2 From 015107ca34ed3f6924c82befab49972298cc895f Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Sat, 17 Aug 2024 15:28:43 -0400 Subject: [PATCH 04/14] add headless arg --- .env.example | 2 ++ helperAPI.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 91823aaf..50e14dee 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,8 @@ DISCORD_CHANNEL= ## OPTIONAL SETTINGS # USE AT YOUR OWN RISK: Wether the bot should wait for a confirmation before executing trades in the CLI DANGER_MODE="false" +# Wether Selenium should run headless (no browser window) +HEADLESS="true" ## BROKER SETTINGS # ALL BROKERS: Separate multiple accounts with different credentials diff --git a/helperAPI.py b/helperAPI.py index 646d46ac..4d06819a 100644 --- a/helperAPI.py +++ b/helperAPI.py @@ -21,6 +21,12 @@ from selenium_stealth import stealth from selenium.webdriver.chrome.service import Service as ChromiumService + +load_dotenv() +DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") +DISCORD_CHANNEL = os.getenv("DISCORD_CHANNEL") +HEADLESS = os.getenv("HEADLESS", "true").lower() == "true" + # Create task queue task_queue = Queue() @@ -491,12 +497,13 @@ def getDriver(DOCKER=False): options.add_argument("--disable-blink-features=AutomationControlled") options.add_argument("--disable-extensions") options.add_argument("--disable-notifications") - options.add_argument("--headless") if DOCKER: # Special Docker options options.add_argument("--disable-dev-shm-usage") options.add_argument("--no-sandbox") options.add_argument("--disable-gpu") + if DOCKER or HEADLESS: + options.add_argument("--headless") driver = webdriver.Chrome( options=options, # Docker uses specific chromedriver installed via apt @@ -529,10 +536,6 @@ def killSeleniumDriver(brokerObj: Brokerage): async def processTasks(message, embed=False): - # Get details from env (they are used prior so we know they exist) - load_dotenv() - DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") - DISCORD_CHANNEL = os.getenv("DISCORD_CHANNEL") # Send message to discord via request post BASE_URL = f"https://discord.com/api/v10/channels/{DISCORD_CHANNEL}/messages" HEADERS = { From 1450dfbf9866e2ddf44ee021ff4f5a262a0c594a Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Sat, 17 Aug 2024 15:48:34 -0400 Subject: [PATCH 05/14] isort --- helperAPI.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helperAPI.py b/helperAPI.py index 4d06819a..6d2bcf25 100644 --- a/helperAPI.py +++ b/helperAPI.py @@ -18,9 +18,8 @@ from discord.ext import commands from dotenv import load_dotenv from selenium import webdriver -from selenium_stealth import stealth from selenium.webdriver.chrome.service import Service as ChromiumService - +from selenium_stealth import stealth load_dotenv() DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") From e04b957e9e7abff794a193f098c9a00c01754731 Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Sat, 17 Aug 2024 15:53:19 -0400 Subject: [PATCH 06/14] fix anti-pattern in chase --- chaseAPI.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chaseAPI.py b/chaseAPI.py index 25b4d246..e7e12a4d 100644 --- a/chaseAPI.py +++ b/chaseAPI.py @@ -214,8 +214,8 @@ def chase_transaction(chase_o: Brokerage, orderObj: stockOrder, loop=None): loop, ) if ( - not messages["ORDER INVALID"] - == "No invalid order message found." + messages["ORDER INVALID"] + != "No invalid order message found." ): printAndDiscord( f"{key} account {account}: The order verification produced the following messages: {messages['ORDER INVALID']}", @@ -239,8 +239,8 @@ def chase_transaction(chase_o: Brokerage, orderObj: stockOrder, loop=None): loop, ) if ( - not messages["ORDER INVALID"] - == "No invalid order message found." + messages["ORDER INVALID"] + != "No invalid order message found." ): printAndDiscord( f"{key} account {account}: The order verification produced the following messages: {messages['ORDER INVALID']}", From f2be90dd63eb3c2c4b6244c7cda6047beb846287 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2024 19:54:37 +0000 Subject: [PATCH 07/14] style: format code with Black and isort This commit fixes the style issues introduced in 88a1e7e according to the output from Black and isort. Details: None --- autoRSA.py | 11 ++++++++--- fidelityAPI.py | 8 ++++---- helperAPI.py | 28 ++++++++++++++++++++-------- vanguardAPI.py | 5 +---- webullAPI.py | 10 +++++++--- 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/autoRSA.py b/autoRSA.py index 37548742..91d3ea57 100644 --- a/autoRSA.py +++ b/autoRSA.py @@ -102,7 +102,10 @@ def fun_run(orderObj: stockOrder, command, botObj=None, loop=None): if broker.lower() == "fidelity": # Fidelity requires docker mode argument, botObj, and loop orderObj.set_logged_in( - globals()[fun_name](DOCKER=DOCKER_MODE, botObj=botObj, loop=loop), broker + globals()[fun_name]( + DOCKER=DOCKER_MODE, botObj=botObj, loop=loop + ), + broker, ) elif broker.lower() in ["fennel", "public"]: # Requires bot object and loop @@ -178,7 +181,9 @@ def argParser(args: list) -> stockOrder: elif args[1] == "day1": orderObj.set_brokers(DAY1_BROKERS) elif args[1] == "most": - orderObj.set_brokers(list(filter(lambda x: x != 'vanguard', SUPPORTED_BROKERS))) + orderObj.set_brokers( + list(filter(lambda x: x != "vanguard", SUPPORTED_BROKERS)) + ) elif args[1] == "fast": orderObj.set_brokers(DAY1_BROKERS + ["robinhood"]) else: @@ -201,7 +206,7 @@ def argParser(args: list) -> stockOrder: elif args[3] == "day1": orderObj.set_brokers(DAY1_BROKERS) elif args[3] == "most": - orderObj.set_brokers(list(filter(lambda x: x != 'vanguard', SUPPORTED_BROKERS))) + orderObj.set_brokers(list(filter(lambda x: x != "vanguard", SUPPORTED_BROKERS))) elif args[3] == "fast": orderObj.set_brokers(DAY1_BROKERS + ["robinhood"]) else: diff --git a/fidelityAPI.py b/fidelityAPI.py index 1b3b15b4..ca74d074 100644 --- a/fidelityAPI.py +++ b/fidelityAPI.py @@ -153,7 +153,9 @@ def fidelity_init(FIDELITY_EXTERNAL=None, DOCKER=False, botObj=None, loop=None): # Make sure the next page loads fully code_field = "#dom-otp-code-input" WebDriverWait(driver, 10).until( - expected_conditions.visibility_of_element_located((By.CSS_SELECTOR, code_field)) + expected_conditions.visibility_of_element_located( + (By.CSS_SELECTOR, code_field) + ) ) # Sometimes codes take a long time to arrive timeout = 300 # 5 minutes @@ -167,9 +169,7 @@ def fidelity_init(FIDELITY_EXTERNAL=None, DOCKER=False, botObj=None, loop=None): else: sms_code = input("Enter security code: ") - code_field = driver.find_element( - by=By.CSS_SELECTOR, value=code_field - ) + code_field = driver.find_element(by=By.CSS_SELECTOR, value=code_field) code_field.send_keys(str(sms_code)) continue_btn_selector = "#dom-otp-code-submit-button" driver.find_element(By.CSS_SELECTOR, continue_btn_selector).click() diff --git a/helperAPI.py b/helperAPI.py index 6d2bcf25..705f699a 100644 --- a/helperAPI.py +++ b/helperAPI.py @@ -392,7 +392,9 @@ def updater(): print() return print(f"Update complete! Now using commit {str(repo.head.commit)[:7]}") - print(f"Check if you're up to date here: https://github.com/NelsonDane/auto-rsa/commits/{repo.active_branch}") + print( + f"Check if you're up to date here: https://github.com/NelsonDane/auto-rsa/commits/{repo.active_branch}" + ) print() return @@ -545,11 +547,17 @@ async def processTasks(message, embed=False): for i in range(0, embed_length, 25): PAYLOAD = { "content": "" if embed else message, - "embeds": [{ - "title": message["title"] if i == 0 else "", - "color": message["color"], - "fields": message["fields"][i:i + 25] - }] if embed else [] + "embeds": ( + [ + { + "title": message["title"] if i == 0 else "", + "color": message["color"], + "fields": message["fields"][i : i + 25], + } + ] + if embed + else [] + ), } # Keep trying until success success = False @@ -642,7 +650,7 @@ def printHoldings(brokerObj: Brokerage, loop=None, mask=True): EMBED = { "title": f"{brokerObj.get_name()} Holdings", "color": 3447003, - "fields": [] + "fields": [], } print( f"==============================\n{brokerObj.get_name()} Holdings\n==============================" @@ -668,7 +676,11 @@ def printHoldings(brokerObj: Brokerage, loop=None, mask=True): print_string += f"Total: ${format(brokerObj.get_account_totals(key, account), '0.2f')}\n" print(print_string) # If somehow longer than 1024, chop and add ... - field["value"] = print_string[:1020] + "..." if len(print_string) > 1024 else print_string + field["value"] = ( + print_string[:1020] + "..." + if len(print_string) > 1024 + else print_string + ) EMBED["fields"].append(field) # for i in range(30): # EMBED["fields"].append(field) diff --git a/vanguardAPI.py b/vanguardAPI.py index 45bb81b4..0d14da78 100644 --- a/vanguardAPI.py +++ b/vanguardAPI.py @@ -112,10 +112,7 @@ def vanguard_holdings(vanguard_o: Brokerage, loop=None): for stock in all_accounts.accounts_positions[account][ account_type ]: - if ( - float(stock["quantity"]) != 0 - and stock["symbol"] != "—" - ): + if float(stock["quantity"]) != 0 and stock["symbol"] != "—": vanguard_o.set_holdings( key, account, diff --git a/webullAPI.py b/webullAPI.py index c1f060a2..01497b20 100644 --- a/webullAPI.py +++ b/webullAPI.py @@ -153,7 +153,9 @@ def webull_transaction(wbo: Brokerage, orderObj: stockOrder, loop=None): askList = quote.get("askList", []) bidList = quote.get("bidList", []) if askList == [] and bidList == []: - printAndDiscord(f"{key}: {s} is not available for trading", loop) + printAndDiscord( + f"{key}: {s} is not available for trading", loop + ) raise Exception(f"{s} is not available for trading") askPrice = float(askList[0]["price"]) if askList != [] else 0 bidPrice = float(bidList[0]["price"]) if bidList != [] else 0 @@ -162,9 +164,11 @@ def webull_transaction(wbo: Brokerage, orderObj: stockOrder, loop=None): # amount < 100 and price < $1 # amount < 1000 and price < $0.10 if ( - (askPrice < 1 or bidPrice < 1) and orderObj.get_amount() < 100 + (askPrice < 1 or bidPrice < 1) + and orderObj.get_amount() < 100 ) or ( - (askPrice < 0.1 or bidPrice < 0.1) and orderObj.get_amount() < 1000 + (askPrice < 0.1 or bidPrice < 0.1) + and orderObj.get_amount() < 1000 ): should_dance = True if should_dance and orderObj.get_action() == "buy": From 30cad145a22dab02b4358d646c2a9e2d644578d4 Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:11:30 -0400 Subject: [PATCH 08/14] remove old commented code --- helperAPI.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/helperAPI.py b/helperAPI.py index 705f699a..5cda03ce 100644 --- a/helperAPI.py +++ b/helperAPI.py @@ -682,8 +682,5 @@ def printHoldings(brokerObj: Brokerage, loop=None, mask=True): else print_string ) EMBED["fields"].append(field) - # for i in range(30): - # EMBED["fields"].append(field) - printAndDiscord(EMBED, loop, True) print("==============================") From 7b4d2ba7ffa403b2e99b9f47d7344224982fe985 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2024 20:13:23 +0000 Subject: [PATCH 09/14] style: format code with Black and isort This commit fixes the style issues introduced in e60a216 according to the output from Black and isort. Details: None --- helperAPI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helperAPI.py b/helperAPI.py index 5cda03ce..a9476862 100644 --- a/helperAPI.py +++ b/helperAPI.py @@ -494,7 +494,7 @@ def getDriver(DOCKER=False): options = webdriver.ChromeOptions() options.add_argument("start-maximized") options.add_experimental_option("excludeSwitches", ["enable-automation"]) - options.add_experimental_option('useAutomationExtension', False) + options.add_experimental_option("useAutomationExtension", False) options.add_argument("--disable-blink-features=AutomationControlled") options.add_argument("--disable-extensions") options.add_argument("--disable-notifications") From ba4745ab4d374ffc9ae456c82e2e8c7fe462d749 Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:55:48 -0400 Subject: [PATCH 10/14] update discord bot instructions --- guides/discordBot.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/guides/discordBot.md b/guides/discordBot.md index e53f028a..24cfce44 100644 --- a/guides/discordBot.md +++ b/guides/discordBot.md @@ -4,11 +4,10 @@ In order to use this bot in Discord, you have to create a bot account and invite 1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) and click `New Application`. 2. Enter a name for your bot, like `AutoRSA` and click `Create`. 3. Click on `Bot` in the left sidebar, then give it a name and profile picture if you want. -4. Disable `Public Bot`. -5. Under `Privileged Gateway Intents`, enable `Message Content Intent`. -6. Click on `OAuth2` in the left sidebar, then `URL Generator`. Then scroll down to `OAuth2 URL Generator`. -7. Under `Scopes` select `bot`. Then underneath that in `Bot Permissions` select `Send Messages` and `Read Message History`. -8. Copy the link in the `Scopes` section and paste it into your browser. Select the server you want to add the bot to and click `Authorize`. +4. Under `Privileged Gateway Intents`, enable `Message Content Intent`. +5. Click on `OAuth2` in the left sidebar, then `URL Generator`. Then scroll down to `OAuth2 URL Generator`. +6. Under `Scopes` select `bot`. Then underneath that in `Bot Permissions` select `Send Messages` and `Read Message History`. +7. Under `Integration Type`, select `Guild Install`. Then copy the link in the `Scopes` section and paste it into your browser. Select the server you want to add the bot to and click `Authorize`. The bot should then appear in your server! 9. Click on `Bot` in the left sidebar. Under `Token`, click `Reset Token`. Copy the new token and paste it into your `.env` file as `DISCORD_TOKEN`. 10. To get the Channel ID, go to `Advanced` in Discord settings, then turn on `Developer Mode`. Then right click on the channel for the bot and click `Copy ID`. Paste the ID into your `.env` file as `DISCORD_CHANNEL`. If you want to turn off `Developer Mode`, you can do so, but it isn't necessary. From cf1f9b9c0a397b019bfd567a91c4e4e5c4aa9140 Mon Sep 17 00:00:00 2001 From: maxxrk Date: Sun, 18 Aug 2024 16:01:41 -0500 Subject: [PATCH 11/14] finish readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index a3bda451..90882f2c 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,11 @@ Required `.env` variables: `.env` file format: - `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. ### Public Made by yours truly using using [public-invest-api](https://github.com/NelsonDane/public-invest-api). Consider giving me a ⭐ From fafc56d6f0994b0bef7ea2d11c78a6689690ba60 Mon Sep 17 00:00:00 2001 From: maxxrk Date: Sun, 18 Aug 2024 18:30:25 -0500 Subject: [PATCH 12/14] fix ft input code --- autoRSA.py | 2 +- firstradeAPI.py | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) 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 d15524a6..4636399c 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,16 @@ 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,16 +33,25 @@ def firstrade_init(FIRSTRADE_EXTERNAL=None): firstrade = ft_account.FTSession( username = account[0], password = account[1], - pin = account[2] if len(account[2]) == 4 else None, - phone = account[2][-4:] if len(account[2]) == 10 else None, + 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/", ) need_code = firstrade.login() if need_code: - code = input("Please enter the pin sent to your email/phone: ") - firstrade.login_two(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) From c4c771111d8d9ad61ab95110e5566fcaae47169e Mon Sep 17 00:00:00 2001 From: maxxrk Date: Sun, 18 Aug 2024 18:59:21 -0500 Subject: [PATCH 13/14] fix style issues --- firstradeAPI.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/firstradeAPI.py b/firstradeAPI.py index 4636399c..5bca0ef2 100644 --- a/firstradeAPI.py +++ b/firstradeAPI.py @@ -13,6 +13,7 @@ from helperAPI import Brokerage, getOTPCodeDiscord, maskString, printAndDiscord, printHoldings, stockOrder + def firstrade_init(botObj=None, loop=None): # Initialize .env file load_dotenv() @@ -31,12 +32,12 @@ def firstrade_init(botObj=None, loop=None): try: account = account.split(":") firstrade = ft_account.FTSession( - username = account[0], - password = account[1], - 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, + username=account[0], + password=account[1], + 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/", ) need_code = firstrade.login() @@ -53,7 +54,7 @@ def firstrade_init(botObj=None, loop=None): ) firstrade.login_two(sms_code) print("Logged in to Firstrade!") - account_info = ft_account.FTAccountData(firstrade) + account_info = ft_account.FTAccountData(firstrade) firstrade_obj.set_logged_in_object(name, firstrade) for account in account_info.account_numbers: firstrade_obj.set_account_number(name, account) @@ -138,7 +139,7 @@ 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(order_conf) printAndDiscord( From 6959782bcf0ef3d6c8d8379d67f5f8ea65dd7dfa Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Sun, 18 Aug 2024 23:03:58 -0400 Subject: [PATCH 14/14] add to .env.example --- .env.example | 2 +- README.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 90882f2c..42ce9531 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,6 @@ More detailed guides for some of the difficult setups: All brokers: separate account credentials with a colon (":"). For example, `SCHWAB_USERNAME:SCHWAB_PASSWORD`. Separate multiple logins with the same broker with a comma (","). For example, `SCHWAB_USERNAME:SCHWAB_PASSWORD,SCHWAB_USERNAME2:SCHWAB_PASSWORD2`. - Some brokerages require `Playwright` to run. On Windows, the `playwright install` command might not be recognized. If this is the case, run `python -m playwright install` instead. #### Chase @@ -197,12 +196,15 @@ Required `.env` variables: `.env` file format: - `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 ⭐