From 0d81012845b4142c6fadfad11ed54ee20b438c51 Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Sat, 17 Aug 2024 02:03:18 -0500 Subject: [PATCH 01/16] Tornado Support --- autoRSA.py | 9 +- tornadoAPI.py | 421 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 tornadoAPI.py diff --git a/autoRSA.py b/autoRSA.py index 37548742..16123f39 100644 --- a/autoRSA.py +++ b/autoRSA.py @@ -31,6 +31,8 @@ from tradierAPI import * from vanguardAPI import * from webullAPI import * + from sofiAPI import * + from tornadoAPI import * except Exception as e: print(f"Error importing libraries: {e}") print(traceback.format_exc()) @@ -54,6 +56,8 @@ "tradier", "vanguard", "webull", + "sofi", + "tornado", ] DAY1_BROKERS = [ "chase", @@ -64,6 +68,7 @@ "tastytrade", "tradier", "webull", + "sofi", ] DISCORD_BOT = False DOCKER_MODE = False @@ -128,9 +133,11 @@ def fun_run(orderObj: stockOrder, command, botObj=None, loop=None): + fun_name + ": Function did not complete successfully." ) + elif broker.lower() == "tornado": + # Initialize Tornado + orderObj.set_logged_in(tornado_init(), broker) else: orderObj.set_logged_in(globals()[fun_name](), broker) - print() if broker.lower() not in ["chase", "vanguard"]: # Verify broker is logged in diff --git a/tornadoAPI.py b/tornadoAPI.py new file mode 100644 index 00000000..0574b1bf --- /dev/null +++ b/tornadoAPI.py @@ -0,0 +1,421 @@ +import datetime +import os +import traceback +from time import sleep +import logging + +from dotenv import load_dotenv +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException, TimeoutException +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait + +from helperAPI import ( + Brokerage, + check_if_page_loaded, + getDriver, + killSeleniumDriver, + printAndDiscord, + printHoldings, + stockOrder, +) + +load_dotenv() + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def tornado_error(driver, e, loop=None): + driver.save_screenshot(f"Tornado-error-{datetime.datetime.now()}.png") + printAndDiscord(f"Tornado Error: {traceback.format_exc()}", loop, embed=False) + + +def tornado_init(TORNADO_EXTERNAL=None, loop=None): + load_dotenv() + + if not os.getenv("TORNADO") and TORNADO_EXTERNAL is None: + printAndDiscord("TORNADO environment variable not found.", loop) + return None + + accounts = ( + os.environ["TORNADO"].strip().split(",") + if TORNADO_EXTERNAL is None + else TORNADO_EXTERNAL.strip().split(",") + ) + TORNADO_obj = Brokerage("TORNADO") + + for index, account in enumerate(accounts): + account_name = f"Tornado {index + 1}" + try: + driver = getDriver(DOCKER=False) + if driver is None: + raise Exception("Driver not found.") + driver.get('https://tornado.com/app/login') + WebDriverWait(driver, 30).until(check_if_page_loaded) + + # Log in with email and password + try: + email_field = WebDriverWait(driver, 30).until( + EC.element_to_be_clickable((By.ID, "email-field"))) + email_field.send_keys(account.split(":")[0]) + + password_field = WebDriverWait(driver, 30).until( + EC.element_to_be_clickable((By.ID, "password-field"))) + password_field.send_keys(account.split(":")[1]) + + login_button = WebDriverWait(driver, 30).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, "#root > div > div > div > div:nth-child(2) > div > div > div > div > form > div.sc-WZYut.ZaYjk > button"))) + login_button.click() + + # Check for the element after logging in to ensure the page is fully loaded + WebDriverWait(driver, 60).until( + EC.presence_of_element_located((By.XPATH, "//*[@id='main-router']/div/div/div/div[1]/div/div/div/div[1]/div[1]/div/span")) + ) + + TORNADO_obj.set_logged_in_object(account_name, driver) + + # Set the account name + TORNADO_obj.set_account_number(account_name, account_name) + logger.info(f"Set account name for {account_name}") + + except TimeoutException: + printAndDiscord(f"TimeoutException: Login failed for {account_name}.", loop) + return False + + except Exception as e: + tornado_error(driver, e, loop) + driver.close() + driver.quit() + return None + return TORNADO_obj + + +def tornado_extract_holdings(driver): + holdings_data = [] + try: + # Locate all the individual stock holdings elements + holdings_elements = driver.find_elements(By.XPATH, ".//div[@class='sc-jEWLvH evXkie']") + + logger.info("Found %d holdings elements to process.", len(holdings_elements)) + + for holding_element in holdings_elements: + try: + # Extract the stock ticker + stock_ticker = holding_element.find_element(By.XPATH, ".//a[1]/div[1]/span").text.strip() + + # Extract the number of shares available + shares = holding_element.find_element(By.XPATH, ".//a[4]/div/div/span/span").text.strip() + shares_float = float(shares.replace(" sh", "")) + + # Corrected XPath to extract the actual stock price + price = holding_element.find_element(By.XPATH, ".//a[1]/div[3]/span/div/div[1]/span").text.strip() + price_float = float(price.replace('$', '').replace(',', '')) + + logger.info("Scraped holding: %s, Shares: %s, Price: %s", stock_ticker, shares_float, price_float) + + # Store the extracted data in a dictionary + holdings_data.append({ + 'stock_ticker': stock_ticker, + 'shares': shares_float, + 'price': price_float + }) + + except Exception as e: + logger.error("Error scraping a holding element: %s", e) + continue + + except Exception as e: + logger.error("Error extracting holdings: %s", e) + return [] + + return holdings_data + + +def tornado_holdings(TORNADO_o: Brokerage, loop=None): + try: + # Ensure we are using the correct account name + account_names = TORNADO_o.get_account_numbers() + for account_name in account_names: + driver: webdriver = TORNADO_o.get_logged_in_objects(account_name) + + logger.info(f"Processing holdings for {account_name}") + + # Fetch the total account value + account_value_element = WebDriverWait(driver, 60).until( + EC.presence_of_element_located((By.XPATH, "//*[@id='main-router']/div/div/div/div[1]/div/div/div[1]/div[1]/div[1]/div/span")) + ) + account_value = account_value_element.text.strip() + account_value_float = float(account_value.replace('$', '').replace(',', '')) + + # Extract holdings data + holdings_data = tornado_extract_holdings(driver) + + for holding in holdings_data: + TORNADO_o.set_holdings(account_name, account_name, holding['stock_ticker'], holding['shares'], holding['price']) + + # Set the account total using the fetched account value + TORNADO_o.set_account_totals(account_name, account_name, account_value_float) + + except Exception as e: + logger.error(f"Error processing Tornado holdings: %s", e) + printAndDiscord(f"Tornado Account: Error processing holdings: {e}", loop) + + logger.info("Finished processing Tornado account, sending holdings to Discord.") + printHoldings(TORNADO_o, loop) # Send the holdings to Discord + killSeleniumDriver(TORNADO_o) # Close the browser after processing + logger.info("Completed Tornado holdings processing.") + + +def tornado_transaction(TORNADO_o: Brokerage, orderObj: stockOrder, loop=None): + print("\n==============================") + print("TORNADO") + print("==============================\n") + + for s in orderObj.get_stocks(): + for key in TORNADO_o.get_account_numbers(): + driver = TORNADO_o.get_logged_in_objects(key) + + # Check if already on the dashboard after login + current_url = driver.current_url + if "dashboard" not in current_url: + driver.get("https://tornado.com/app/dashboard") + print(f"Navigated to Tornado dashboard page for account {key}") + sleep(3) # Added delay to ensure page loads + else: + print(f"Already on the Tornado dashboard page for account {key}") + sleep(2) # Slight delay for stability + + try: + # Wait for the search bar and interact with it + search_field = WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, '#nav_securities_search'))) + print("Search bar found, attempting to enter stock symbol.") + search_field.click() + sleep(1) + search_field.send_keys(s) + print(f"Entered stock symbol {s} into the search bar") + except TimeoutException: + print(f"Search field for {s} not found.") + printAndDiscord(f"Tornado search field not found for {s}.", loop) + continue + + try: + # Wait for the search results to load + WebDriverWait(driver, 10).until( + EC.presence_of_all_elements_located((By.XPATH, '//*[@id="nav_securities_search_container"]/div[2]/ul/li')) + ) + dropdown_items = driver.find_elements(By.XPATH, '//*[@id="nav_securities_search_container"]/div[2]/ul/li') + total_items = len(dropdown_items) + print(f"Found {total_items} search results for {s}") + sleep(2) # Added delay to ensure the dropdown items load + + if total_items == 0: + print(f"No stock found for {s}. Moving to next stock.") + printAndDiscord(f"Tornado doesn't have {s}.", loop) + continue + + found_stock = False + for item in dropdown_items: + ticker_name = item.find_element(By.CLASS_NAME, 'bold').text.strip() # Extract ticker name + if ticker_name == s: + found_stock = True + sleep(1) # Short pause before clicking the correct stock + item.click() + print(f"Found and selected stock {s}") + break + + if not found_stock: + print(f"Tornado doesn't have {s}. Moving to next stock.") + printAndDiscord(f"Tornado doesn't have {s}.", loop) + continue + except TimeoutException: + print(f"Search results did not appear for {s}. Moving to next stock.") + printAndDiscord(f"Tornado search results did not appear for {s}.", loop) + continue + + # Proceed with the transaction based on the action (buy/sell) + if orderObj.get_action() == "buy": + handle_buy(driver, s, orderObj, loop) + elif orderObj.get_action() == "sell": + handle_sell(driver, s, orderObj, loop) + + # Return to the dashboard + try: + dashboard_link = WebDriverWait(driver, 30).until( + EC.presence_of_element_located((By.XPATH, '//*[@id="root"]/div/div/div[4]/div/div[1]/div/div/div[2]/div[1]/div[2]/span[1]/a/span'))) + dashboard_link.click() + WebDriverWait(driver, 60).until( + EC.presence_of_element_located((By.XPATH, '//*[@id="main-router"]/div/div/div/div[1]/div/div/div/div[1]/div[1]/div/span')) + ) + print(f"Returned to dashboard after processing {s}") + except TimeoutException: + print(f"Failed to return to dashboard after processing {s}.") + printAndDiscord(f"Tornado failed to return to dashboard after processing {s}.", loop) + + print("Completed all transactions, Exiting...") + driver.close() # Close the driver window + driver.quit() # Quit the driver entirely + + +def handle_buy(driver, stock, orderObj, loop): + DRY = orderObj.get_dry() + QUANTITY = orderObj.get_amount() + print("DRY MODE:", DRY) + + try: + buy_button = WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.XPATH, '//*[@id="buy-button"]'))) + driver.execute_script("arguments[0].click();", buy_button) + print("Buy button clicked") + except TimeoutException: + print(f"Buy button not found for {stock}. Moving to next stock.") + printAndDiscord(f"Tornado buy button not found for {stock}.", loop) + return + + try: + print(f"Entering quantity {QUANTITY}") + quant = WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[3]/input'))) + quant.clear() + quant.send_keys(str(QUANTITY)) + print(f"Quantity {QUANTITY} entered") + except TimeoutException: + print(f"Failed to enter quantity for {stock}. Moving to next stock.") + printAndDiscord(f"Tornado failed to enter quantity for {stock}.", loop) + return + + # Now check for current shares and adjust XPaths accordingly + try: + current_shares_element = driver.find_element(By.XPATH, '//*[@id="main-router"]/div[1]/div/div[4]/div') + current_shares_text = current_shares_element.text.strip() + print(f"Current shares for {stock}: {current_shares_text}") + has_current_shares = True + except NoSuchElementException: + print(f"No current shares for {stock}.") + has_current_shares = False + + market_order_xpath = '//*[@id="main-router"]/div[1]/div/div[5]/select' if has_current_shares else '//*[@id="main-router"]/div[1]/div/div[4]/select' + current_price_xpath = '//*[@id="main-router"]/div[1]/div/div[6]/div' if has_current_shares else '//*[@id="main-router"]/div[1]/div/div[5]/div' + buy_power_xpath = '//*[@id="main-router"]/div[1]/div/div[8]/div' if has_current_shares else '//*[@id="main-router"]/div[1]/div/div[7]/div' + + try: + market_order_option = WebDriverWait(driver, 20).until( + EC.presence_of_element_located((By.XPATH, market_order_xpath)) + ) + market_order_option.click() + print("Market order selected") + except TimeoutException: + print(f"Failed to select market order for {stock}. Moving to next stock.") + printAndDiscord(f"Tornado failed to select market order for {stock}.", loop) + return + + try: + buy_power = driver.find_element(By.XPATH, buy_power_xpath).text.strip() + cost = driver.find_element(By.XPATH, current_price_xpath).text.strip() + + buy_power_float = float(buy_power.replace('$', '').replace(',', '')) + cost_float = float(cost.replace('$', '').replace(',', '')) + + if buy_power_float < cost_float: + print(f"Insufficient funds to complete the purchase for {stock}.") + printAndDiscord(f"Tornado insufficient funds to buy {stock}. Required: ${cost_float}, Available: ${buy_power_float}", loop) + return + + print(f"Buying power: ${buy_power_float}, Cost: ${cost_float}") + except TimeoutException: + print(f"Failed to fetch buying power or cost for {stock}. Moving to next stock.") + printAndDiscord(f"Tornado failed to fetch buying power or cost for {stock}.", loop) + return + + if not DRY: + print("Pausing for 5 seconds before submitting the order...") + + try: + submit_button = WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[10]/div/button'))) + submit_button.click() + print(f"Successfully bought {QUANTITY} shares of {stock}") + printAndDiscord(f"Tornado account: buy {QUANTITY} shares of {stock} at {cost}", loop) + except TimeoutException: + print(f"Failed to submit buy order for {stock}. Moving to next stock.") + printAndDiscord(f"Tornado failed to submit buy order for {stock}.", loop) + else: + sleep(5) + print(f"DRY MODE: Simulated order BUY for {QUANTITY} shares of {stock} at {cost}") + printAndDiscord(f"Tornado account: dry run buy {QUANTITY} shares of {stock} at {cost}", loop) + + +def handle_sell(driver, stock, orderObj, loop): + DRY = orderObj.get_dry() + QUANTITY = orderObj.get_amount() + print("DRY MODE:", DRY) + + try: + sell_button = WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.XPATH, '//*[@id="sell-button"]'))) + driver.execute_script("arguments[0].click();", sell_button) + print("Sell button clicked") + except TimeoutException: + print(f"Sell button not found for {stock}. Moving to next stock.") + printAndDiscord(f"Tornado sell button not found for {stock}.", loop) + return + + try: + current_shares_element = driver.find_element(By.XPATH, '//*[@id="main-router"]/div[1]/div/div[4]/div') + current_shares = float(current_shares_element.text.strip().replace(" sh", "")) + print(f"Current shares for {stock}: {current_shares}") + except NoSuchElementException: + print(f"No current shares found for {stock}. Unable to sell.") + printAndDiscord(f"Tornado no current shares to sell for {stock}.", loop) + return + + if QUANTITY > current_shares: + print(f"Not enough shares to sell for {stock}. Available: {current_shares}") + printAndDiscord(f"Tornado not enough shares to sell {stock}. Available: {current_shares}", loop) + return + + try: + quant = WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[3]/input'))) + quant.clear() + quant.send_keys(str(QUANTITY)) + print(f"Quantity {QUANTITY} entered") + except TimeoutException: + print(f"Failed to enter quantity for {stock}. Moving to next stock.") + printAndDiscord(f"Tornado failed to enter quantity for {stock}.", loop) + return + + try: + market_order_option = WebDriverWait(driver, 20).until( + EC.presence_of_element_located((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[6]/select/option[1]')) + ) + market_order_option.click() + print("Market order selected") + except TimeoutException: + print(f"Failed to select market order for {stock}. Moving to next stock.") + printAndDiscord(f"Tornado failed to select market order for {stock}.", loop) + return + + try: + sell_price = driver.find_element(By.XPATH, '//*[@id="main-router"]/div[1]/div/div[7]/div').text.strip() + print(f"Sell price for {stock}: {sell_price}") + except TimeoutException: + print(f"Failed to fetch sell price for {stock}. Moving to next stock.") + printAndDiscord(f"Tornado failed to fetch sell price for {stock}.", loop) + return + + if not DRY: + try: + submit_button = WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[11]/div/button'))) + submit_button.click() + print(f"Successfully sold {QUANTITY} shares of {stock}") + printAndDiscord(f"Tornado account: sell {QUANTITY} shares of {stock} at {sell_price}", loop) + except TimeoutException: + print(f"Failed to submit sell order for {stock}. Moving to next stock.") + printAndDiscord(f"Tornado failed to submit sell order for {stock}.", loop) + else: + sleep(5) + print(f"DRY MODE: Simulated order SELL for {QUANTITY} shares of {stock} at {sell_price}") + printAndDiscord(f"Tornado account: dry run sell {QUANTITY} shares of {stock} at {sell_price}", loop) From 8a5aa47106baad49dfb65ee06e8178f24e7c7931 Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Sat, 17 Aug 2024 02:04:28 -0500 Subject: [PATCH 02/16] Update .env.example --- .env.example | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 91823aaf..65cff558 100644 --- a/.env.example +++ b/.env.example @@ -61,4 +61,8 @@ VANGUARD= # Webull # WEBULL=WEBULL_USERNAME:WEBULL_PASSWORD:WEBULL_DID:WEBULL_TRADING_PIN -WEBULL= \ No newline at end of file +WEBULL= + +#TORNADO +# TORNADO=TORNADO_EMAIL:TORNADO_PASSWORD +TORNADO= \ No newline at end of file From 6c7a7c7b768523a5ea8259eb0f7b9147313a564a Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Sat, 17 Aug 2024 15:29:54 -0500 Subject: [PATCH 03/16] fixed account holding error and autorsa fixed the issue where it would flood the terminal when the presence of zero stocks inside the account holdings, made it a little cleaner. Also fixed the autoRSA showing sofi inside. --- autoRSA.py | 13 ++++--------- tornadoAPI.py | 10 +++++++++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/autoRSA.py b/autoRSA.py index 16123f39..983af2a1 100644 --- a/autoRSA.py +++ b/autoRSA.py @@ -28,11 +28,10 @@ from robinhoodAPI import * from schwabAPI import * from tastyAPI import * + from tornadoAPI import * from tradierAPI import * from vanguardAPI import * from webullAPI import * - from sofiAPI import * - from tornadoAPI import * except Exception as e: print(f"Error importing libraries: {e}") print(traceback.format_exc()) @@ -53,11 +52,10 @@ "robinhood", "schwab", "tastytrade", + "tornado", "tradier", "vanguard", "webull", - "sofi", - "tornado", ] DAY1_BROKERS = [ "chase", @@ -68,7 +66,6 @@ "tastytrade", "tradier", "webull", - "sofi", ] DISCORD_BOT = False DOCKER_MODE = False @@ -138,6 +135,7 @@ def fun_run(orderObj: stockOrder, command, botObj=None, loop=None): orderObj.set_logged_in(tornado_init(), broker) else: orderObj.set_logged_in(globals()[fun_name](), broker) + print() if broker.lower() not in ["chase", "vanguard"]: # Verify broker is logged in @@ -379,10 +377,7 @@ async def restart(ctx): print() await ctx.send("Restarting...") await bot.close() - if DOCKER_MODE: - os._exit(0) # Special exit code to restart docker container - else: - os.execv(sys.executable, [sys.executable] + sys.argv) + os._exit(0) # Special exit code to restart docker container # Catch bad commands @bot.event diff --git a/tornadoAPI.py b/tornadoAPI.py index 0574b1bf..d3c69c2f 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -97,6 +97,10 @@ def tornado_extract_holdings(driver): # Locate all the individual stock holdings elements holdings_elements = driver.find_elements(By.XPATH, ".//div[@class='sc-jEWLvH evXkie']") + if len(holdings_elements) == 0: + logger.warning("No holdings found in the account.") + return holdings_data + logger.info("Found %d holdings elements to process.", len(holdings_elements)) for holding_element in holdings_elements: @@ -151,6 +155,10 @@ def tornado_holdings(TORNADO_o: Brokerage, loop=None): # Extract holdings data holdings_data = tornado_extract_holdings(driver) + if not holdings_data: + logger.warning(f"No holdings found for {account_name}. Skipping account.") + continue # Skip to the next account + for holding in holdings_data: TORNADO_o.set_holdings(account_name, account_name, holding['stock_ticker'], holding['shares'], holding['price']) @@ -158,7 +166,7 @@ def tornado_holdings(TORNADO_o: Brokerage, loop=None): TORNADO_o.set_account_totals(account_name, account_name, account_value_float) except Exception as e: - logger.error(f"Error processing Tornado holdings: %s", e) + logger.error(f"Error processing Tornado holdings: {e}") printAndDiscord(f"Tornado Account: Error processing holdings: {e}", loop) logger.info("Finished processing Tornado account, sending holdings to Discord.") From cd1c92dbfec9948b7e93e3b88aedde2e6eba8b98 Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Sat, 17 Aug 2024 23:51:53 -0500 Subject: [PATCH 04/16] Update tornadoAPI.py cleaned some style things --- tornadoAPI.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tornadoAPI.py b/tornadoAPI.py index d3c69c2f..b867ca86 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -26,7 +26,8 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -def tornado_error(driver, e, loop=None): + +def tornado_error(driver, loop=None): driver.save_screenshot(f"Tornado-error-{datetime.datetime.now()}.png") printAndDiscord(f"Tornado Error: {traceback.format_exc()}", loop, embed=False) @@ -44,7 +45,7 @@ def tornado_init(TORNADO_EXTERNAL=None, loop=None): else TORNADO_EXTERNAL.strip().split(",") ) TORNADO_obj = Brokerage("TORNADO") - + for index, account in enumerate(accounts): account_name = f"Tornado {index + 1}" try: @@ -77,7 +78,7 @@ def tornado_init(TORNADO_EXTERNAL=None, loop=None): # Set the account name TORNADO_obj.set_account_number(account_name, account_name) - logger.info(f"Set account name for {account_name}") + logger.info("Set account name for %s", account_name) except TimeoutException: printAndDiscord(f"TimeoutException: Login failed for {account_name}.", loop) @@ -143,7 +144,7 @@ def tornado_holdings(TORNADO_o: Brokerage, loop=None): for account_name in account_names: driver: webdriver = TORNADO_o.get_logged_in_objects(account_name) - logger.info(f"Processing holdings for {account_name}") + logger.info("Processing holdings for %s", account_name) # Fetch the total account value account_value_element = WebDriverWait(driver, 60).until( @@ -156,7 +157,7 @@ def tornado_holdings(TORNADO_o: Brokerage, loop=None): holdings_data = tornado_extract_holdings(driver) if not holdings_data: - logger.warning(f"No holdings found for {account_name}. Skipping account.") + logger.warning("No holdings found for %s. Skipping account.", account_name) continue # Skip to the next account for holding in holdings_data: @@ -166,7 +167,7 @@ def tornado_holdings(TORNADO_o: Brokerage, loop=None): TORNADO_o.set_account_totals(account_name, account_name, account_value_float) except Exception as e: - logger.error(f"Error processing Tornado holdings: {e}") + logger.error("Error processing Tornado holdings: %s", e) printAndDiscord(f"Tornado Account: Error processing holdings: {e}", loop) logger.info("Finished processing Tornado account, sending holdings to Discord.") From 9505258e94934ae4a1214b575f456a17c1d10f19 Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Sat, 17 Aug 2024 23:55:52 -0500 Subject: [PATCH 05/16] Update tornadoAPI.py fixed Too many positional arguments for function call --- tornadoAPI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tornadoAPI.py b/tornadoAPI.py index b867ca86..b5cb5804 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -85,7 +85,7 @@ def tornado_init(TORNADO_EXTERNAL=None, loop=None): return False except Exception as e: - tornado_error(driver, e, loop) + tornado_error(driver, loop) driver.close() driver.quit() return None From 8e5d1f3db8c54c17c8a34c019556eac2c761305d Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Sun, 18 Aug 2024 00:02:17 -0500 Subject: [PATCH 06/16] Update tornadoAPI.py --- tornadoAPI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tornadoAPI.py b/tornadoAPI.py index b5cb5804..da7385a2 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -84,7 +84,7 @@ def tornado_init(TORNADO_EXTERNAL=None, loop=None): printAndDiscord(f"TimeoutException: Login failed for {account_name}.", loop) return False - except Exception as e: + except Exception: tornado_error(driver, loop) driver.close() driver.quit() From fd2368ec3ab9a20b900178e1a1459a082d60ceb0 Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Sun, 18 Aug 2024 21:38:31 -0500 Subject: [PATCH 07/16] fixed formatting issue .env.example had tornado in all caps and when printing holdings it was in all caps as well. --- .env.example | 2 +- tornadoAPI.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 65cff558..56c66949 100644 --- a/.env.example +++ b/.env.example @@ -63,6 +63,6 @@ VANGUARD= # WEBULL=WEBULL_USERNAME:WEBULL_PASSWORD:WEBULL_DID:WEBULL_TRADING_PIN WEBULL= -#TORNADO +#Tornado # TORNADO=TORNADO_EMAIL:TORNADO_PASSWORD TORNADO= \ No newline at end of file diff --git a/tornadoAPI.py b/tornadoAPI.py index da7385a2..52811cd2 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -44,7 +44,7 @@ def tornado_init(TORNADO_EXTERNAL=None, loop=None): if TORNADO_EXTERNAL is None else TORNADO_EXTERNAL.strip().split(",") ) - TORNADO_obj = Brokerage("TORNADO") + TORNADO_obj = Brokerage("Tornado") for index, account in enumerate(accounts): account_name = f"Tornado {index + 1}" From 3cb528718c32fd2f580d7a1a298a0647f86cdf96 Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Wed, 21 Aug 2024 12:48:27 -0500 Subject: [PATCH 08/16] Update tornadoAPI.py fixed elements pushing around depending if theres already stock present in the order page --- tornadoAPI.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tornadoAPI.py b/tornadoAPI.py index 52811cd2..1290dae6 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -44,12 +44,12 @@ def tornado_init(TORNADO_EXTERNAL=None, loop=None): if TORNADO_EXTERNAL is None else TORNADO_EXTERNAL.strip().split(",") ) - TORNADO_obj = Brokerage("Tornado") + TORNADO_obj = Brokerage("TORNADO") for index, account in enumerate(accounts): account_name = f"Tornado {index + 1}" try: - driver = getDriver(DOCKER=False) + driver = getDriver() if driver is None: raise Exception("Driver not found.") driver.get('https://tornado.com/app/login') @@ -338,11 +338,10 @@ def handle_buy(driver, stock, orderObj, loop): return if not DRY: - print("Pausing for 5 seconds before submitting the order...") try: submit_button = WebDriverWait(driver, 20).until( - EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[10]/div/button'))) + EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[10]/div/button | //*[@id="main-router"]/div[1]/div/div[9]/div/button'))) submit_button.click() print(f"Successfully bought {QUANTITY} shares of {stock}") printAndDiscord(f"Tornado account: buy {QUANTITY} shares of {stock} at {cost}", loop) From 7f94b52a0527db4870f2201c44e650ee745cc5dc Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Wed, 21 Aug 2024 13:23:39 -0500 Subject: [PATCH 09/16] Update tornadoAPI.py Fixed bot not looping to buy all stocks listed and fixed buy/sell confirm button being blocked and breaking the bot. --- tornadoAPI.py | 62 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/tornadoAPI.py b/tornadoAPI.py index 1290dae6..44d6c886 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -185,21 +185,23 @@ def tornado_transaction(TORNADO_o: Brokerage, orderObj: stockOrder, loop=None): for key in TORNADO_o.get_account_numbers(): driver = TORNADO_o.get_logged_in_objects(key) - # Check if already on the dashboard after login - current_url = driver.current_url - if "dashboard" not in current_url: - driver.get("https://tornado.com/app/dashboard") - print(f"Navigated to Tornado dashboard page for account {key}") - sleep(3) # Added delay to ensure page loads - else: - print(f"Already on the Tornado dashboard page for account {key}") - sleep(2) # Slight delay for stability + # Ensure we are on the Tornado dashboard or navigate to it + try: + current_url = driver.current_url + if "app" not in current_url: + driver.get("https://tornado.com/app/") + print(f"Navigated to Tornado dashboard page for account {key}") + WebDriverWait(driver, 30).until(check_if_page_loaded) + else: + print(f"Already on the Tornado dashboard page for account {key}") + except Exception as e: + print(f"Failed to navigate to dashboard for {key}: {e}") + continue try: - # Wait for the search bar and interact with it + # Interact with the search bar search_field = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.CSS_SELECTOR, '#nav_securities_search'))) - print("Search bar found, attempting to enter stock symbol.") search_field.click() sleep(1) search_field.send_keys(s) @@ -210,14 +212,14 @@ def tornado_transaction(TORNADO_o: Brokerage, orderObj: stockOrder, loop=None): continue try: - # Wait for the search results to load + # Wait for and process search results WebDriverWait(driver, 10).until( EC.presence_of_all_elements_located((By.XPATH, '//*[@id="nav_securities_search_container"]/div[2]/ul/li')) ) dropdown_items = driver.find_elements(By.XPATH, '//*[@id="nav_securities_search_container"]/div[2]/ul/li') total_items = len(dropdown_items) print(f"Found {total_items} search results for {s}") - sleep(2) # Added delay to ensure the dropdown items load + sleep(2) if total_items == 0: print(f"No stock found for {s}. Moving to next stock.") @@ -226,10 +228,10 @@ def tornado_transaction(TORNADO_o: Brokerage, orderObj: stockOrder, loop=None): found_stock = False for item in dropdown_items: - ticker_name = item.find_element(By.CLASS_NAME, 'bold').text.strip() # Extract ticker name + ticker_name = item.find_element(By.CLASS_NAME, 'bold').text.strip() if ticker_name == s: found_stock = True - sleep(1) # Short pause before clicking the correct stock + sleep(1) item.click() print(f"Found and selected stock {s}") break @@ -249,10 +251,10 @@ def tornado_transaction(TORNADO_o: Brokerage, orderObj: stockOrder, loop=None): elif orderObj.get_action() == "sell": handle_sell(driver, s, orderObj, loop) - # Return to the dashboard + # Ensure to return to the dashboard after every transaction try: dashboard_link = WebDriverWait(driver, 30).until( - EC.presence_of_element_located((By.XPATH, '//*[@id="root"]/div/div/div[4]/div/div[1]/div/div/div[2]/div[1]/div[2]/span[1]/a/span'))) + EC.element_to_be_clickable((By.XPATH, '//*[@id="root"]/div/div/div[4]/div/div[1]/div/div/div[2]/div[1]/div[2]/span[1]/a/span'))) dashboard_link.click() WebDriverWait(driver, 60).until( EC.presence_of_element_located((By.XPATH, '//*[@id="main-router"]/div/div/div/div[1]/div/div/div/div[1]/div[1]/div/span')) @@ -263,8 +265,8 @@ def tornado_transaction(TORNADO_o: Brokerage, orderObj: stockOrder, loop=None): printAndDiscord(f"Tornado failed to return to dashboard after processing {s}.", loop) print("Completed all transactions, Exiting...") - driver.close() # Close the driver window - driver.quit() # Quit the driver entirely + driver.close() + driver.quit() def handle_buy(driver, stock, orderObj, loop): @@ -338,16 +340,21 @@ def handle_buy(driver, stock, orderObj, loop): return if not DRY: - try: submit_button = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[10]/div/button | //*[@id="main-router"]/div[1]/div/div[9]/div/button'))) submit_button.click() print(f"Successfully bought {QUANTITY} shares of {stock}") printAndDiscord(f"Tornado account: buy {QUANTITY} shares of {stock} at {cost}", loop) + + # Click the "Continue" button after placing the order + continue_button = WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[2]/div/button'))) + continue_button.click() + print("Clicked the Continue button after placing the order.") except TimeoutException: - print(f"Failed to submit buy order for {stock}. Moving to next stock.") - printAndDiscord(f"Tornado failed to submit buy order for {stock}.", loop) + print(f"Failed to submit buy order for {stock} or click Continue. Moving to next stock.") + printAndDiscord(f"Tornado failed to submit buy order for {stock} or click Continue.", loop) else: sleep(5) print(f"DRY MODE: Simulated order BUY for {QUANTITY} shares of {stock} at {cost}") @@ -420,10 +427,15 @@ def handle_sell(driver, stock, orderObj, loop): submit_button.click() print(f"Successfully sold {QUANTITY} shares of {stock}") printAndDiscord(f"Tornado account: sell {QUANTITY} shares of {stock} at {sell_price}", loop) + + # Click the "Continue" button after placing the order + continue_button = WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[2]/div/button'))) + continue_button.click() + print("Clicked the Continue button after placing the order.") except TimeoutException: - print(f"Failed to submit sell order for {stock}. Moving to next stock.") - printAndDiscord(f"Tornado failed to submit sell order for {stock}.", loop) + print(f"Failed to submit sell order for {stock} or click Continue. Moving to next stock.") + printAndDiscord(f"Tornado failed to submit sell order for {stock} or click Continue.", loop) else: - sleep(5) print(f"DRY MODE: Simulated order SELL for {QUANTITY} shares of {stock} at {sell_price}") printAndDiscord(f"Tornado account: dry run sell {QUANTITY} shares of {stock} at {sell_price}", loop) From c688f518e4c49522bfe0a4805be9250bb25deddc Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Thu, 22 Aug 2024 19:27:48 -0500 Subject: [PATCH 10/16] style updates I think this is the correct style --- tornadoAPI.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tornadoAPI.py b/tornadoAPI.py index 44d6c886..e83c2efb 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -36,7 +36,7 @@ def tornado_init(TORNADO_EXTERNAL=None, loop=None): load_dotenv() if not os.getenv("TORNADO") and TORNADO_EXTERNAL is None: - printAndDiscord("TORNADO environment variable not found.", loop) + printAndDiscord("Tornado environment variable not found.", loop) return None accounts = ( @@ -44,7 +44,7 @@ def tornado_init(TORNADO_EXTERNAL=None, loop=None): if TORNADO_EXTERNAL is None else TORNADO_EXTERNAL.strip().split(",") ) - TORNADO_obj = Brokerage("TORNADO") + TORNADO_obj = Brokerage("Tornado") for index, account in enumerate(accounts): account_name = f"Tornado {index + 1}" @@ -137,12 +137,12 @@ def tornado_extract_holdings(driver): return holdings_data -def tornado_holdings(TORNADO_o: Brokerage, loop=None): +def tornado_holdings(Tornado_o: Brokerage, loop=None): try: # Ensure we are using the correct account name - account_names = TORNADO_o.get_account_numbers() + account_names = Tornado_o.get_account_numbers() for account_name in account_names: - driver: webdriver = TORNADO_o.get_logged_in_objects(account_name) + driver: webdriver = Tornado_o.get_logged_in_objects(account_name) logger.info("Processing holdings for %s", account_name) @@ -161,29 +161,29 @@ def tornado_holdings(TORNADO_o: Brokerage, loop=None): continue # Skip to the next account for holding in holdings_data: - TORNADO_o.set_holdings(account_name, account_name, holding['stock_ticker'], holding['shares'], holding['price']) + Tornado_o.set_holdings(account_name, account_name, holding['stock_ticker'], holding['shares'], holding['price']) # Set the account total using the fetched account value - TORNADO_o.set_account_totals(account_name, account_name, account_value_float) + Tornado_o.set_account_totals(account_name, account_name, account_value_float) except Exception as e: logger.error("Error processing Tornado holdings: %s", e) printAndDiscord(f"Tornado Account: Error processing holdings: {e}", loop) logger.info("Finished processing Tornado account, sending holdings to Discord.") - printHoldings(TORNADO_o, loop) # Send the holdings to Discord - killSeleniumDriver(TORNADO_o) # Close the browser after processing + printHoldings(Tornado_o, loop) # Send the holdings to Discord + killSeleniumDriver(Tornado_o) # Close the browser after processing logger.info("Completed Tornado holdings processing.") -def tornado_transaction(TORNADO_o: Brokerage, orderObj: stockOrder, loop=None): +def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): print("\n==============================") - print("TORNADO") + print("Tornado") print("==============================\n") for s in orderObj.get_stocks(): - for key in TORNADO_o.get_account_numbers(): - driver = TORNADO_o.get_logged_in_objects(key) + for key in Tornado_o.get_account_numbers(): + driver = Tornado_o.get_logged_in_objects(key) # Ensure we are on the Tornado dashboard or navigate to it try: From 6a324853567889c40748844d72b774961afd9000 Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Thu, 22 Aug 2024 20:20:56 -0500 Subject: [PATCH 11/16] deepsource style and format --- tornadoAPI.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tornadoAPI.py b/tornadoAPI.py index e83c2efb..988b3b08 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -346,7 +346,7 @@ def handle_buy(driver, stock, orderObj, loop): submit_button.click() print(f"Successfully bought {QUANTITY} shares of {stock}") printAndDiscord(f"Tornado account: buy {QUANTITY} shares of {stock} at {cost}", loop) - + # Click the "Continue" button after placing the order continue_button = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[2]/div/button'))) @@ -427,7 +427,7 @@ def handle_sell(driver, stock, orderObj, loop): submit_button.click() print(f"Successfully sold {QUANTITY} shares of {stock}") printAndDiscord(f"Tornado account: sell {QUANTITY} shares of {stock} at {sell_price}", loop) - + # Click the "Continue" button after placing the order continue_button = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[2]/div/button'))) From 4f136637c7f9c3099b467c7527512b7c865b7bd1 Mon Sep 17 00:00:00 2001 From: ImNotOssy Date: Sat, 31 Aug 2024 23:17:55 -0500 Subject: [PATCH 12/16] Clean up Lots of parts were cleaned up, thanks to @matthew55 for suggesting these changes Co-Authored-By: Matthew <78285385+matthew55@users.noreply.github.com> --- .env.example | 18 +++++++++--------- autoRSA.py | 5 ++++- tornadoAPI.py | 40 ++++++++++++++++++++++++++++------------ 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/.env.example b/.env.example index 1c3d16a6..5469e22c 100644 --- a/.env.example +++ b/.env.example @@ -49,22 +49,22 @@ ROBINHOOD= # SCHWAB=SCHWAB_USERNAME:SCHWAB_PASSWORD:NA SCHWAB= -# Tradier -# TRADIER=TRADIER_ACCESS_TOKEN -TRADIER= - # Tastytrade # TASTYTRADE=TASTYTRADE_USERNAME:TASTYTRADE_PASSWORD TASTYTRADE= +#Tornado +# TORNADO=TORNADO_EMAIL:TORNADO_PASSWORD +TORNADO= + +# Tradier +# TRADIER=TRADIER_ACCESS_TOKEN +TRADIER= + # Vanguard # VANGUARD=VANGUARD_USERNAME:VANGUARD_PASSWORD:PHONE_LAST_FOUR:DEBUG(Optional) TRUE/FALSE VANGUARD= # Webull # WEBULL=WEBULL_USERNAME:WEBULL_PASSWORD:WEBULL_DID:WEBULL_TRADING_PIN -WEBULL= - -#Tornado -# TORNADO=TORNADO_EMAIL:TORNADO_PASSWORD -TORNADO= \ No newline at end of file +WEBULL= \ No newline at end of file diff --git a/autoRSA.py b/autoRSA.py index e87eb6d7..85633c63 100644 --- a/autoRSA.py +++ b/autoRSA.py @@ -389,7 +389,10 @@ async def restart(ctx): print() await ctx.send("Restarting...") await bot.close() - os._exit(0) # Special exit code to restart docker container + if DOCKER_MODE: + os._exit(0) # Special exit code to restart docker container + else: + os.execv(sys.executable, [sys.executable] + sys.argv) # Catch bad commands @bot.event diff --git a/tornadoAPI.py b/tornadoAPI.py index 988b3b08..089ed0be 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -39,11 +39,11 @@ def tornado_init(TORNADO_EXTERNAL=None, loop=None): printAndDiscord("Tornado environment variable not found.", loop) return None - accounts = ( - os.environ["TORNADO"].strip().split(",") - if TORNADO_EXTERNAL is None - else TORNADO_EXTERNAL.strip().split(",") - ) + if TORNADO_EXTERNAL is None: + accounts = os.environ["TORNADO"].strip().split(",") + else: + accounts = TORNADO_EXTERNAL.strip().split(",") + TORNADO_obj = Brokerage("Tornado") for index, account in enumerate(accounts): @@ -126,12 +126,12 @@ def tornado_extract_holdings(driver): 'price': price_float }) - except Exception as e: - logger.error("Error scraping a holding element: %s", e) + except Exception: + tornado_error(driver) continue - except Exception as e: - logger.error("Error extracting holdings: %s", e) + except Exception: + tornado_error(driver) return [] return holdings_data @@ -167,7 +167,7 @@ def tornado_holdings(Tornado_o: Brokerage, loop=None): Tornado_o.set_account_totals(account_name, account_name, account_value_float) except Exception as e: - logger.error("Error processing Tornado holdings: %s", e) + tornado_error(driver, loop) printAndDiscord(f"Tornado Account: Error processing holdings: {e}", loop) logger.info("Finished processing Tornado account, sending holdings to Discord.") @@ -195,6 +195,7 @@ def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): else: print(f"Already on the Tornado dashboard page for account {key}") except Exception as e: + tornado_error(driver, loop) print(f"Failed to navigate to dashboard for {key}: {e}") continue @@ -207,6 +208,7 @@ def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): search_field.send_keys(s) print(f"Entered stock symbol {s} into the search bar") except TimeoutException: + tornado_error(driver, loop) print(f"Search field for {s} not found.") printAndDiscord(f"Tornado search field not found for {s}.", loop) continue @@ -241,6 +243,7 @@ def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): printAndDiscord(f"Tornado doesn't have {s}.", loop) continue except TimeoutException: + tornado_error(driver, loop) print(f"Search results did not appear for {s}. Moving to next stock.") printAndDiscord(f"Tornado search results did not appear for {s}.", loop) continue @@ -261,12 +264,12 @@ def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): ) print(f"Returned to dashboard after processing {s}") except TimeoutException: + tornado_error(driver, loop) print(f"Failed to return to dashboard after processing {s}.") printAndDiscord(f"Tornado failed to return to dashboard after processing {s}.", loop) print("Completed all transactions, Exiting...") - driver.close() - driver.quit() + killSeleniumDriver(driver) def handle_buy(driver, stock, orderObj, loop): @@ -280,6 +283,7 @@ def handle_buy(driver, stock, orderObj, loop): driver.execute_script("arguments[0].click();", buy_button) print("Buy button clicked") except TimeoutException: + tornado_error(driver, loop) print(f"Buy button not found for {stock}. Moving to next stock.") printAndDiscord(f"Tornado buy button not found for {stock}.", loop) return @@ -292,6 +296,7 @@ def handle_buy(driver, stock, orderObj, loop): quant.send_keys(str(QUANTITY)) print(f"Quantity {QUANTITY} entered") except TimeoutException: + tornado_error(driver, loop) print(f"Failed to enter quantity for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to enter quantity for {stock}.", loop) return @@ -303,6 +308,7 @@ def handle_buy(driver, stock, orderObj, loop): print(f"Current shares for {stock}: {current_shares_text}") has_current_shares = True except NoSuchElementException: + tornado_error(driver, loop) print(f"No current shares for {stock}.") has_current_shares = False @@ -317,6 +323,7 @@ def handle_buy(driver, stock, orderObj, loop): market_order_option.click() print("Market order selected") except TimeoutException: + tornado_error(driver, loop) print(f"Failed to select market order for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to select market order for {stock}.", loop) return @@ -329,12 +336,14 @@ def handle_buy(driver, stock, orderObj, loop): cost_float = float(cost.replace('$', '').replace(',', '')) if buy_power_float < cost_float: + tornado_error(driver, loop) print(f"Insufficient funds to complete the purchase for {stock}.") printAndDiscord(f"Tornado insufficient funds to buy {stock}. Required: ${cost_float}, Available: ${buy_power_float}", loop) return print(f"Buying power: ${buy_power_float}, Cost: ${cost_float}") except TimeoutException: + tornado_error(driver, loop) print(f"Failed to fetch buying power or cost for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to fetch buying power or cost for {stock}.", loop) return @@ -353,6 +362,7 @@ def handle_buy(driver, stock, orderObj, loop): continue_button.click() print("Clicked the Continue button after placing the order.") except TimeoutException: + tornado_error(driver, loop) print(f"Failed to submit buy order for {stock} or click Continue. Moving to next stock.") printAndDiscord(f"Tornado failed to submit buy order for {stock} or click Continue.", loop) else: @@ -372,6 +382,7 @@ def handle_sell(driver, stock, orderObj, loop): driver.execute_script("arguments[0].click();", sell_button) print("Sell button clicked") except TimeoutException: + tornado_error(driver, loop) print(f"Sell button not found for {stock}. Moving to next stock.") printAndDiscord(f"Tornado sell button not found for {stock}.", loop) return @@ -381,6 +392,7 @@ def handle_sell(driver, stock, orderObj, loop): current_shares = float(current_shares_element.text.strip().replace(" sh", "")) print(f"Current shares for {stock}: {current_shares}") except NoSuchElementException: + tornado_error(driver, loop) print(f"No current shares found for {stock}. Unable to sell.") printAndDiscord(f"Tornado no current shares to sell for {stock}.", loop) return @@ -397,6 +409,7 @@ def handle_sell(driver, stock, orderObj, loop): quant.send_keys(str(QUANTITY)) print(f"Quantity {QUANTITY} entered") except TimeoutException: + tornado_error(driver, loop) print(f"Failed to enter quantity for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to enter quantity for {stock}.", loop) return @@ -408,6 +421,7 @@ def handle_sell(driver, stock, orderObj, loop): market_order_option.click() print("Market order selected") except TimeoutException: + tornado_error(driver, loop) print(f"Failed to select market order for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to select market order for {stock}.", loop) return @@ -416,6 +430,7 @@ def handle_sell(driver, stock, orderObj, loop): sell_price = driver.find_element(By.XPATH, '//*[@id="main-router"]/div[1]/div/div[7]/div').text.strip() print(f"Sell price for {stock}: {sell_price}") except TimeoutException: + tornado_error(driver, loop) print(f"Failed to fetch sell price for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to fetch sell price for {stock}.", loop) return @@ -434,6 +449,7 @@ def handle_sell(driver, stock, orderObj, loop): continue_button.click() print("Clicked the Continue button after placing the order.") except TimeoutException: + tornado_error(driver, loop) print(f"Failed to submit sell order for {stock} or click Continue. Moving to next stock.") printAndDiscord(f"Tornado failed to submit sell order for {stock} or click Continue.", loop) else: From e3d08a479ee638906b12644ed1aba591690d3a3b Mon Sep 17 00:00:00 2001 From: matthew55 <78285385+matthew55@users.noreply.github.com> Date: Sun, 1 Sep 2024 01:04:11 -0400 Subject: [PATCH 13/16] Add Tornado to README --- .env.example | 2 +- README.md | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 5469e22c..3c57ec4d 100644 --- a/.env.example +++ b/.env.example @@ -53,7 +53,7 @@ SCHWAB= # TASTYTRADE=TASTYTRADE_USERNAME:TASTYTRADE_PASSWORD TASTYTRADE= -#Tornado +# Tornado # TORNADO=TORNADO_EMAIL:TORNADO_PASSWORD TORNADO= diff --git a/README.md b/README.md index 00cec843..5474d05e 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,16 @@ To get your TOTP secret, follow this [guide](guides/schwabSetup.md). Note 1: Think or Swim must be enabled on all accounts. To enable, go to `Trade` > `Trading Platforms` > `Learn how to enable thinkorswim`. Then press `Continue` and expand the `thinkorswim Access Agreement` and accept it. Then press `Continue` again. Then select the checkbox for all available accounts and press `Submit`. It may take a day or two for the accounts to be enabled. +### Tornado +Made by [ImNotOssy](https://github.com/ImNotOssy) using Selenium. Go give him a follow 👇 + +Required `.env` variables: +- `TORNADO_EMAIL` +- `TORNADO_PASSWORD` + +`.env` file format: +- `TORNADO=TORNADO_EMAIL:TORNADO_PASSWORD` + ### Tradier Made by yours truly using the official [Tradier API](https://documentation.tradier.com/brokerage-api/trading/getting-started). Consider giving me a ⭐ From d4eda94953480f0688818b714e331bec97d35b6f Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Sun, 1 Sep 2024 17:25:51 -0400 Subject: [PATCH 14/16] change to star --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5474d05e..4a01048c 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ To get your TOTP secret, follow this [guide](guides/schwabSetup.md). Note 1: Think or Swim must be enabled on all accounts. To enable, go to `Trade` > `Trading Platforms` > `Learn how to enable thinkorswim`. Then press `Continue` and expand the `thinkorswim Access Agreement` and accept it. Then press `Continue` again. Then select the checkbox for all available accounts and press `Submit`. It may take a day or two for the accounts to be enabled. ### Tornado -Made by [ImNotOssy](https://github.com/ImNotOssy) using Selenium. Go give him a follow 👇 +Made by [ImNotOssy](https://github.com/ImNotOssy) using Selenium. Go give them a ⭐ Required `.env` variables: - `TORNADO_EMAIL` From 217bd9793646a8c0bed586448e9e07ad5f0cfb03 Mon Sep 17 00:00:00 2001 From: matthew55 <78285385+matthew55@users.noreply.github.com> Date: Sun, 1 Sep 2024 18:35:34 -0400 Subject: [PATCH 15/16] Remove unnecessary logging functions --- tornadoAPI.py | 66 ++++----------------------------------------------- 1 file changed, 5 insertions(+), 61 deletions(-) diff --git a/tornadoAPI.py b/tornadoAPI.py index 089ed0be..d2edf629 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -2,7 +2,6 @@ import os import traceback from time import sleep -import logging from dotenv import load_dotenv from selenium import webdriver @@ -23,9 +22,6 @@ load_dotenv() -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - def tornado_error(driver, loop=None): driver.save_screenshot(f"Tornado-error-{datetime.datetime.now()}.png") @@ -78,7 +74,6 @@ def tornado_init(TORNADO_EXTERNAL=None, loop=None): # Set the account name TORNADO_obj.set_account_number(account_name, account_name) - logger.info("Set account name for %s", account_name) except TimeoutException: printAndDiscord(f"TimeoutException: Login failed for {account_name}.", loop) @@ -99,11 +94,8 @@ def tornado_extract_holdings(driver): holdings_elements = driver.find_elements(By.XPATH, ".//div[@class='sc-jEWLvH evXkie']") if len(holdings_elements) == 0: - logger.warning("No holdings found in the account.") return holdings_data - logger.info("Found %d holdings elements to process.", len(holdings_elements)) - for holding_element in holdings_elements: try: # Extract the stock ticker @@ -117,8 +109,6 @@ def tornado_extract_holdings(driver): price = holding_element.find_element(By.XPATH, ".//a[1]/div[3]/span/div/div[1]/span").text.strip() price_float = float(price.replace('$', '').replace(',', '')) - logger.info("Scraped holding: %s, Shares: %s, Price: %s", stock_ticker, shares_float, price_float) - # Store the extracted data in a dictionary holdings_data.append({ 'stock_ticker': stock_ticker, @@ -144,7 +134,7 @@ def tornado_holdings(Tornado_o: Brokerage, loop=None): for account_name in account_names: driver: webdriver = Tornado_o.get_logged_in_objects(account_name) - logger.info("Processing holdings for %s", account_name) + print(f"Processing holdings for {account_name}") # Fetch the total account value account_value_element = WebDriverWait(driver, 60).until( @@ -157,7 +147,7 @@ def tornado_holdings(Tornado_o: Brokerage, loop=None): holdings_data = tornado_extract_holdings(driver) if not holdings_data: - logger.warning("No holdings found for %s. Skipping account.", account_name) + print(f"No holdings found for {account_name}. Skipping account.") continue # Skip to the next account for holding in holdings_data: @@ -170,10 +160,8 @@ def tornado_holdings(Tornado_o: Brokerage, loop=None): tornado_error(driver, loop) printAndDiscord(f"Tornado Account: Error processing holdings: {e}", loop) - logger.info("Finished processing Tornado account, sending holdings to Discord.") printHoldings(Tornado_o, loop) # Send the holdings to Discord killSeleniumDriver(Tornado_o) # Close the browser after processing - logger.info("Completed Tornado holdings processing.") def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): @@ -190,13 +178,10 @@ def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): current_url = driver.current_url if "app" not in current_url: driver.get("https://tornado.com/app/") - print(f"Navigated to Tornado dashboard page for account {key}") WebDriverWait(driver, 30).until(check_if_page_loaded) - else: - print(f"Already on the Tornado dashboard page for account {key}") except Exception as e: tornado_error(driver, loop) - print(f"Failed to navigate to dashboard for {key}: {e}") + printAndDiscord(f"Failed to navigate to dashboard for {key}: {e}", loop) continue try: @@ -206,10 +191,8 @@ def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): search_field.click() sleep(1) search_field.send_keys(s) - print(f"Entered stock symbol {s} into the search bar") except TimeoutException: tornado_error(driver, loop) - print(f"Search field for {s} not found.") printAndDiscord(f"Tornado search field not found for {s}.", loop) continue @@ -220,11 +203,9 @@ def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): ) dropdown_items = driver.find_elements(By.XPATH, '//*[@id="nav_securities_search_container"]/div[2]/ul/li') total_items = len(dropdown_items) - print(f"Found {total_items} search results for {s}") sleep(2) if total_items == 0: - print(f"No stock found for {s}. Moving to next stock.") printAndDiscord(f"Tornado doesn't have {s}.", loop) continue @@ -235,16 +216,13 @@ def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): found_stock = True sleep(1) item.click() - print(f"Found and selected stock {s}") break if not found_stock: - print(f"Tornado doesn't have {s}. Moving to next stock.") printAndDiscord(f"Tornado doesn't have {s}.", loop) continue except TimeoutException: tornado_error(driver, loop) - print(f"Search results did not appear for {s}. Moving to next stock.") printAndDiscord(f"Tornado search results did not appear for {s}.", loop) continue @@ -262,10 +240,8 @@ def tornado_transaction(Tornado_o: Brokerage, orderObj: stockOrder, loop=None): WebDriverWait(driver, 60).until( EC.presence_of_element_located((By.XPATH, '//*[@id="main-router"]/div/div/div/div[1]/div/div/div/div[1]/div[1]/div/span')) ) - print(f"Returned to dashboard after processing {s}") except TimeoutException: tornado_error(driver, loop) - print(f"Failed to return to dashboard after processing {s}.") printAndDiscord(f"Tornado failed to return to dashboard after processing {s}.", loop) print("Completed all transactions, Exiting...") @@ -281,23 +257,18 @@ def handle_buy(driver, stock, orderObj, loop): buy_button = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.XPATH, '//*[@id="buy-button"]'))) driver.execute_script("arguments[0].click();", buy_button) - print("Buy button clicked") except TimeoutException: tornado_error(driver, loop) - print(f"Buy button not found for {stock}. Moving to next stock.") printAndDiscord(f"Tornado buy button not found for {stock}.", loop) return try: - print(f"Entering quantity {QUANTITY}") quant = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[3]/input'))) quant.clear() quant.send_keys(str(QUANTITY)) - print(f"Quantity {QUANTITY} entered") except TimeoutException: tornado_error(driver, loop) - print(f"Failed to enter quantity for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to enter quantity for {stock}.", loop) return @@ -305,11 +276,9 @@ def handle_buy(driver, stock, orderObj, loop): try: current_shares_element = driver.find_element(By.XPATH, '//*[@id="main-router"]/div[1]/div/div[4]/div') current_shares_text = current_shares_element.text.strip() - print(f"Current shares for {stock}: {current_shares_text}") has_current_shares = True except NoSuchElementException: tornado_error(driver, loop) - print(f"No current shares for {stock}.") has_current_shares = False market_order_xpath = '//*[@id="main-router"]/div[1]/div/div[5]/select' if has_current_shares else '//*[@id="main-router"]/div[1]/div/div[4]/select' @@ -321,10 +290,8 @@ def handle_buy(driver, stock, orderObj, loop): EC.presence_of_element_located((By.XPATH, market_order_xpath)) ) market_order_option.click() - print("Market order selected") except TimeoutException: tornado_error(driver, loop) - print(f"Failed to select market order for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to select market order for {stock}.", loop) return @@ -337,14 +304,11 @@ def handle_buy(driver, stock, orderObj, loop): if buy_power_float < cost_float: tornado_error(driver, loop) - print(f"Insufficient funds to complete the purchase for {stock}.") printAndDiscord(f"Tornado insufficient funds to buy {stock}. Required: ${cost_float}, Available: ${buy_power_float}", loop) return - print(f"Buying power: ${buy_power_float}, Cost: ${cost_float}") except TimeoutException: tornado_error(driver, loop) - print(f"Failed to fetch buying power or cost for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to fetch buying power or cost for {stock}.", loop) return @@ -353,52 +317,42 @@ def handle_buy(driver, stock, orderObj, loop): submit_button = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[10]/div/button | //*[@id="main-router"]/div[1]/div/div[9]/div/button'))) submit_button.click() - print(f"Successfully bought {QUANTITY} shares of {stock}") printAndDiscord(f"Tornado account: buy {QUANTITY} shares of {stock} at {cost}", loop) # Click the "Continue" button after placing the order continue_button = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[2]/div/button'))) continue_button.click() - print("Clicked the Continue button after placing the order.") except TimeoutException: tornado_error(driver, loop) - print(f"Failed to submit buy order for {stock} or click Continue. Moving to next stock.") printAndDiscord(f"Tornado failed to submit buy order for {stock} or click Continue.", loop) else: sleep(5) - print(f"DRY MODE: Simulated order BUY for {QUANTITY} shares of {stock} at {cost}") - printAndDiscord(f"Tornado account: dry run buy {QUANTITY} shares of {stock} at {cost}", loop) + printAndDiscord(f"DRY MODE: Simulated order BUY for {QUANTITY} shares of {stock} at {cost}", loop) def handle_sell(driver, stock, orderObj, loop): DRY = orderObj.get_dry() QUANTITY = orderObj.get_amount() - print("DRY MODE:", DRY) try: sell_button = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.XPATH, '//*[@id="sell-button"]'))) driver.execute_script("arguments[0].click();", sell_button) - print("Sell button clicked") except TimeoutException: tornado_error(driver, loop) - print(f"Sell button not found for {stock}. Moving to next stock.") printAndDiscord(f"Tornado sell button not found for {stock}.", loop) return try: current_shares_element = driver.find_element(By.XPATH, '//*[@id="main-router"]/div[1]/div/div[4]/div') current_shares = float(current_shares_element.text.strip().replace(" sh", "")) - print(f"Current shares for {stock}: {current_shares}") except NoSuchElementException: tornado_error(driver, loop) - print(f"No current shares found for {stock}. Unable to sell.") printAndDiscord(f"Tornado no current shares to sell for {stock}.", loop) return if QUANTITY > current_shares: - print(f"Not enough shares to sell for {stock}. Available: {current_shares}") printAndDiscord(f"Tornado not enough shares to sell {stock}. Available: {current_shares}", loop) return @@ -407,10 +361,8 @@ def handle_sell(driver, stock, orderObj, loop): EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[3]/input'))) quant.clear() quant.send_keys(str(QUANTITY)) - print(f"Quantity {QUANTITY} entered") except TimeoutException: tornado_error(driver, loop) - print(f"Failed to enter quantity for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to enter quantity for {stock}.", loop) return @@ -419,19 +371,15 @@ def handle_sell(driver, stock, orderObj, loop): EC.presence_of_element_located((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[6]/select/option[1]')) ) market_order_option.click() - print("Market order selected") except TimeoutException: tornado_error(driver, loop) - print(f"Failed to select market order for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to select market order for {stock}.", loop) return try: sell_price = driver.find_element(By.XPATH, '//*[@id="main-router"]/div[1]/div/div[7]/div').text.strip() - print(f"Sell price for {stock}: {sell_price}") except TimeoutException: tornado_error(driver, loop) - print(f"Failed to fetch sell price for {stock}. Moving to next stock.") printAndDiscord(f"Tornado failed to fetch sell price for {stock}.", loop) return @@ -440,18 +388,14 @@ def handle_sell(driver, stock, orderObj, loop): submit_button = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[11]/div/button'))) submit_button.click() - print(f"Successfully sold {QUANTITY} shares of {stock}") printAndDiscord(f"Tornado account: sell {QUANTITY} shares of {stock} at {sell_price}", loop) # Click the "Continue" button after placing the order continue_button = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.XPATH, '//*[@id="main-router"]/div[1]/div/div[2]/div/button'))) continue_button.click() - print("Clicked the Continue button after placing the order.") except TimeoutException: tornado_error(driver, loop) - print(f"Failed to submit sell order for {stock} or click Continue. Moving to next stock.") printAndDiscord(f"Tornado failed to submit sell order for {stock} or click Continue.", loop) else: - print(f"DRY MODE: Simulated order SELL for {QUANTITY} shares of {stock} at {sell_price}") - printAndDiscord(f"Tornado account: dry run sell {QUANTITY} shares of {stock} at {sell_price}", loop) + printAndDiscord(f"DRY MODE: Simulated order SELL for {QUANTITY} shares of {stock} at {sell_price}", loop) From 591cf7a753661b1f7ae95e1f35fa36e30eb51bc3 Mon Sep 17 00:00:00 2001 From: matthew55 <78285385+matthew55@users.noreply.github.com> Date: Sun, 1 Sep 2024 18:41:26 -0400 Subject: [PATCH 16/16] Remove unused variable --- tornadoAPI.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tornadoAPI.py b/tornadoAPI.py index d2edf629..15ee8522 100644 --- a/tornadoAPI.py +++ b/tornadoAPI.py @@ -274,8 +274,6 @@ def handle_buy(driver, stock, orderObj, loop): # Now check for current shares and adjust XPaths accordingly try: - current_shares_element = driver.find_element(By.XPATH, '//*[@id="main-router"]/div[1]/div/div[4]/div') - current_shares_text = current_shares_element.text.strip() has_current_shares = True except NoSuchElementException: tornado_error(driver, loop)