diff --git a/.env.example b/.env.example index 6307bc2f..91823aaf 100644 --- a/.env.example +++ b/.env.example @@ -55,6 +55,10 @@ TRADIER= # TASTYTRADE=TASTYTRADE_USERNAME:TASTYTRADE_PASSWORD TASTYTRADE= +# 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= \ No newline at end of file diff --git a/.gitignore b/.gitignore index ef18a474..7c76335b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ src/ test* *venv/ .vscode/ +*.zip diff --git a/README.md b/README.md index c4858b9d..ec3073d6 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,21 @@ Your `WEBULL_USERNAME` can be your email or phone number. If using a phone numbe To get your Webull DID, follow this [guide](https://github.com/tedchou12/webull/wiki/Workaround-for-Login-%E2%80%90-Method-2). +### Vanguard +Made by [MaxxRK](https://github.com/MaxxRK/) using the [vanguard-api](https://github.com/MaxxRK/vanguard-api). Go give them a ⭐ + +Required `.env` variables: +- `VANGUARD_USERNAME` +- `VANGUARD_PASSWORD` +- `CELL_PHONE_LAST_FOUR` + +Optional `.env` variables: +- `DEBUG` (Set to `True` to enable debug mode) + +`.env` file format: +- `VANGUARD=VANGUARD_USERNAME:VANGUARD_PASSWORD:PHONE_LAST_FOUR:DEBUG` + + ### 🤷‍♂️ Maybe future brokerages 🤷‍♀️ #### Vanguard In progress [here](https://github.com/NelsonDane/auto-rsa/pull/242). diff --git a/autoRSA.py b/autoRSA.py index 2ddd7a02..85d20278 100644 --- a/autoRSA.py +++ b/autoRSA.py @@ -29,6 +29,7 @@ from schwabAPI import * from tastyAPI import * from tradierAPI import * + from vanguardAPI import * from webullAPI import * except Exception as e: print(f"Error importing libraries: {e}") @@ -51,6 +52,7 @@ "schwab", "tastytrade", "tradier", + "vanguard", "webull", ] DAY1_BROKERS = [ @@ -79,6 +81,8 @@ def nicknames(broker): return "robinhood" if broker == "tasty": return "tastytrade" + if broker == "vg": + return "vanguard" if broker == "wb": return "webull" return broker @@ -106,7 +110,7 @@ def fun_run(orderObj: stockOrder, command, botObj=None, loop=None): orderObj.set_logged_in( globals()[fun_name](botObj=botObj, loop=loop), broker ) - elif broker.lower() == "chase": + elif broker.lower() in ["chase", "vanguard"]: fun_name = broker + "_run" # PLAYWRIGHT_BROKERS have to run all transactions with one function th = ThreadHandler( @@ -129,7 +133,7 @@ def fun_run(orderObj: stockOrder, command, botObj=None, loop=None): orderObj.set_logged_in(globals()[fun_name](), broker) print() - if broker.lower() != "chase": + if broker.lower() not in ["chase", "vanguard"]: # Verify broker is logged in orderObj.order_validate(preLogin=False) logged_in_broker = orderObj.get_logged_in(broker) diff --git a/requirements.txt b/requirements.txt index 3cfa18ce..127d927d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,5 @@ schwab-api==0.4.0 selenium==4.21.0 setuptools==70.0.0 tastytrade==8.0 +vanguard-api==0.1.7 -e git+https://github.com/NelsonDane/webull.git@ef14ae63f9e1436fbea77fe864df54847cf2f730#egg=webull diff --git a/vanguardAPI.py b/vanguardAPI.py new file mode 100644 index 00000000..ec0cc248 --- /dev/null +++ b/vanguardAPI.py @@ -0,0 +1,250 @@ +# Donald Ryan Gullett(MaxxRK) +# Vanguard API + +import asyncio +import os +import pprint +import traceback + +from dotenv import load_dotenv +from vanguard import account as vg_account +from vanguard import order, session + +from helperAPI import ( + Brokerage, + getOTPCodeDiscord, + maskString, + printAndDiscord, + printHoldings, + stockOrder, +) + + +def vanguard_run(orderObj: stockOrder, command=None, botObj=None, loop=None): + # Initialize .env file + load_dotenv() + # Import Vanguard account + if not os.getenv("VANGUARD"): + print("Vanguard not found, skipping...") + return None + accounts = os.environ["VANGUARD"].strip().split(",") + # Set the functions to be run + _, second_command = command + + for account in accounts: + index = accounts.index(account) + 1 + success = vanguard_init( + account=account, + index=index, + botObj=botObj, + loop=loop, + ) + if success is not None: + orderObj.set_logged_in(success, "vanguard") + if second_command == "_holdings": + vanguard_holdings(success, loop=loop) + else: + vanguard_transaction(success, orderObj, loop=loop) + return None + + +def vanguard_init(account, index, botObj=None, loop=None): + # Log in to Vanguard account + print("Logging in to Vanguard...") + vanguard_obj = Brokerage("VANGUARD") + name = f"Vanguard {index}" + try: + account = account.split(":") + debug = bool(account[3]) if len(account) == 4 else False + vg_session = session.VanguardSession( + title=f"Vanguard_{index}", + headless=True, + profile_path="./creds", + debug=debug, + ) + need_second = vg_session.login(account[0], account[1], account[2]) + if need_second: + if botObj is None and loop is None: + vg_session.login_two(input("Enter code: ")) + else: + sms_code = asyncio.run_coroutine_threadsafe( + getOTPCodeDiscord(botObj, name, timeout=120, loop=loop), loop + ).result() + if sms_code is None: + raise Exception( + f"Vanguard {index} code not received in time...", loop + ) + vg_session.login_two(sms_code) + all_accounts = vg_account.AllAccount(vg_session) + success = all_accounts.get_account_ids() + if not success: + raise Exception("Error getting account details") + print("Logged in to Vanguard!") + vanguard_obj.set_logged_in_object(name, vg_session) + print_accounts = [] + for acct in all_accounts.account_totals: + vanguard_obj.set_account_number(name, acct) + vanguard_obj.set_account_totals( + name, acct, all_accounts.account_totals[acct] + ) + print_accounts.append(acct) + print(f"The following Vanguard accounts were found: {print_accounts}") + except Exception as e: + vg_session.close_browser() + print(f"Error logging in to Vanguard: {e}") + print(traceback.format_exc()) + return None + return vanguard_obj + + +def vanguard_holdings(vanguard_o: Brokerage, loop=None): + # Get holdings on each account + for key in vanguard_o.get_account_numbers(): + try: + obj: session.VanguardSession = vanguard_o.get_logged_in_objects(key) + all_accounts = vg_account.AllAccount(obj) + if all_accounts is None: + raise Exception("Error getting account details") + success = all_accounts.get_holdings() + if success: + for account in all_accounts.accounts_positions: + for account_type in all_accounts.accounts_positions[account].keys(): + for stock in all_accounts.accounts_positions[account][ + account_type + ]: + if float(stock["quantity"]) != 0: + vanguard_o.set_holdings( + key, + account, + stock["symbol"], + stock["quantity"], + stock["price"], + ) + else: + raise Exception("Vanguard-api failed to retrieve holdings.") + except Exception as e: + obj.close_browser() + printAndDiscord(f"{key} {account}: Error getting holdings: {e}", loop) + print(traceback.format_exc()) + continue + printHoldings(vanguard_o, loop) + obj.close_browser() + + +def vanguard_transaction(vanguard_o: Brokerage, orderObj: stockOrder, loop=None): + print() + print("==============================") + print("Vanguard") + print("==============================") + print() + # Buy on each account + for s in orderObj.get_stocks(): + for key in vanguard_o.get_account_numbers(): + printAndDiscord( + f"{key} {orderObj.get_action()}ing {orderObj.get_amount()} {s} @ {orderObj.get_price()}", + loop, + ) + try: + for account in vanguard_o.get_account_numbers(key): + print_account = maskString(account) + obj: session.VanguardSession = vanguard_o.get_logged_in_objects(key) + # If DRY is True, don't actually make the transaction + if orderObj.get_dry(): + printAndDiscord( + "Running in DRY mode. No transactions will be made.", loop + ) + vg_order = order.Order(obj) + price_type = order.PriceType.MARKET + if orderObj.get_action().capitalize() == "Buy": + order_type = order.OrderSide.BUY + else: + order_type = order.OrderSide.SELL + messages = vg_order.place_order( + account_id=account, + quantity=int(orderObj.get_amount()), + price_type=price_type, + symbol=s, + duration=order.Duration.DAY, + order_type=order_type, + dry_run=orderObj.get_dry(), + ) + print("The order verification produced the following messages: ") + if ( + messages["ORDER CONFIRMATION"] + == "No order confirmation page found. Order Failed." + ): + printAndDiscord( + "Market order failed placing limit order.", loop + ) + price_type = order.PriceType.LIMIT + price = vg_order.get_quote(s) + 0.01 + messages = vg_order.place_order( + account_id=account, + quantity=int(orderObj.get_amount()), + price_type=price_type, + symbol=s, + duration=order.Duration.DAY, + order_type=order_type, + limit_price=price, + dry_run=orderObj.get_dry(), + ) + if orderObj.get_dry(): + if messages["ORDER PREVIEW"] != "": + pprint.pprint(messages["ORDER PREVIEW"]) + printAndDiscord( + ( + f"{key} account {print_account}: The order verification was " + + ( + "successful" + if messages["ORDER PREVIEW"] + not in ["", "No order preview page found."] + else "unsuccessful" + ) + ), + loop, + ) + if ( + messages["ORDER INVALID"] + != "No invalid order message found." + ): + printAndDiscord( + f"{key} account {print_account}: The order verification produced the following messages: {messages['ORDER INVALID']}", + loop, + ) + else: + if messages["ORDER CONFIRMATION"] != "": + pprint.pprint(messages["ORDER CONFIRMATION"]) + printAndDiscord( + ( + f"{key} account {print_account}: The order verification was " + + ( + "successful" + if messages["ORDER CONFIRMATION"] + not in [ + "", + "No order confirmation page found. Order Failed.", + ] + else "unsuccessful" + ) + ), + loop, + ) + if ( + messages["ORDER INVALID"] + != "No invalid order message found." + ): + printAndDiscord( + f"{key} account {print_account}: The order verification produced the following messages: {messages['ORDER INVALID']}", + loop, + ) + except Exception as e: + printAndDiscord( + f"{key} {print_account}: Error submitting order: {e}", loop + ) + print(traceback.format_exc()) + continue + obj.close_browser() + printAndDiscord( + "All Vanguard transactions complete", + loop, + )