From c65be30588179979d25c3014b2aaeda4f67f7db0 Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Mon, 9 Oct 2023 21:47:56 -0400 Subject: [PATCH 1/5] add types to stockOrder and Brokerage --- helperAPI.py | 128 +++++++++++++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 54 deletions(-) diff --git a/helperAPI.py b/helperAPI.py index 0ed756bb..17236b2c 100644 --- a/helperAPI.py +++ b/helperAPI.py @@ -21,23 +21,23 @@ class stockOrder: def __init__(self): - self.__action = None # Buy or sell - self.__amount = None # Amount of shares to buy/sell - self.__stock = [] # List of stock tickers to buy/sell - self.__time = "day" # Only supports day for now - self.__price = "market" # Default to market price - self.__brokers = [] # List of brokerages to use - self.__notbrokers = [] # List of brokerages to not use !ally - self.__dry = True # Dry run mode - self.__holdings = False # Get holdings from enabled brokerages - self.__logged_in = {} # Dict of logged in brokerage objects - - def set_action(self, action): + self.__action: str = None # Buy or sell + self.__amount: float = None # Amount of shares to buy/sell + self.__stock: list = [] # List of stock tickers to buy/sell + self.__time: str = "day" # Only supports day for now + self.__price: str = "market" # Default to market price + self.__brokers: list = [] # List of brokerages to use + self.__notbrokers: list = [] # List of brokerages to not use !ally + self.__dry: bool = True # Dry run mode + self.__holdings: bool = False # Get holdings from enabled brokerages + self.__logged_in: dict = {} # Dict of logged in brokerage objects + + def set_action(self, action: str) -> None or ValueError: if action.lower() not in ["buy", "sell"]: raise ValueError("Action must be buy or sell") self.__action = action.lower() - def set_amount(self, amount): + def set_amount(self, amount: float) -> None or ValueError: # Only allow floats try: amount = float(amount) @@ -45,19 +45,26 @@ def set_amount(self, amount): raise ValueError(f"Amount ({amount}) must be a number") self.__amount = amount - def set_stock(self, stock): + def set_stock(self, stock: str) -> None or ValueError: # Only allow strings for now if not isinstance(stock, str): raise ValueError("Stock must be a string") self.__stock.append(stock.upper()) - def set_time(self, time): + def set_time(self, time) -> None or NotImplementedError: raise NotImplementedError - def set_price(self, price): - self.__price = float(price) - - def set_brokers(self, brokers): + def set_price(self, price: str or float) -> None or ValueError: + # Only "market" or float + if not isinstance(price, (str, float)): + raise ValueError("Price must be a string or float") + if isinstance(price, float): + price = round(price, 2) + if isinstance(price, str): + price = price.lower() + self.__price = price + + def set_brokers(self, brokers: list) -> None or ValueError: # Only allow strings or lists if not isinstance(brokers, (str, list)): raise ValueError("Brokers must be a string or list") @@ -67,46 +74,56 @@ def set_brokers(self, brokers): else: self.__brokers.append(brokers.lower()) - def set_notbrokers(self, notbrokers): - # Only allow strings for now + def set_notbrokers(self, notbrokers: list) -> None or ValueError: + # Only allow strings or lists if not isinstance(notbrokers, str): raise ValueError("Not Brokers must be a string") - self.__notbrokers.append(notbrokers.lower()) + if isinstance(notbrokers, list): + for b in notbrokers: + self.__notbrokers.append(b.lower()) + else: + self.__notbrokers.append(notbrokers.lower()) - def set_dry(self, dry): + def set_dry(self, dry: bool) -> None or ValueError: + # Only allow bools + if not isinstance(dry, bool): + raise ValueError("Dry must be a boolean") self.__dry = dry - def set_holdings(self, holdings): + def set_holdings(self, holdings: bool) -> None or ValueError: + # Only allow bools + if not isinstance(holdings, bool): + raise ValueError("Holdings must be a boolean") self.__holdings = holdings - def set_logged_in(self, logged_in, broker): + def set_logged_in(self, logged_in, broker: str): self.__logged_in[broker] = logged_in - def get_action(self): + def get_action(self) -> str: return self.__action - def get_amount(self): + def get_amount(self) -> float: return self.__amount - def get_stocks(self): + def get_stocks(self) -> list: return self.__stock - def get_time(self): + def get_time(self) -> str: return self.__time - def get_price(self): + def get_price(self) -> str or float: return self.__price - def get_brokers(self): + def get_brokers(self) -> list: return self.__brokers - def get_notbrokers(self): + def get_notbrokers(self) -> list: return self.__notbrokers - def get_dry(self): + def get_dry(self) -> bool: return self.__dry - def get_holdings(self): + def get_holdings(self) -> bool: return self.__holdings def get_logged_in(self, broker=None): @@ -161,24 +178,26 @@ def __str__(self) -> str: class Brokerage: def __init__(self, name): - self.__name = name # Name of brokerage - self.__account_numbers = ( + self.__name: str = name # Name of brokerage + self.__account_numbers: dict = ( {} ) # Dictionary of account names and numbers under parent - self.__logged_in_objects = {} # Dictionary of logged in objects under parent - self.__holdings = {} # Dictionary of holdings under parent - self.__account_totals = {} # Dictionary of account totals - self.__account_types = {} # Dictionary of account types - - def set_name(self, name): + self.__logged_in_objects: dict = {} # Dictionary of logged in objects under parent + self.__holdings: dict = {} # Dictionary of holdings under parent + self.__account_totals: dict = {} # Dictionary of account totals + self.__account_types: dict = {} # Dictionary of account types + + def set_name(self, name: str): + if not isinstance(name, str): + raise ValueError("Name must be a string") self.__name = name - def set_account_number(self, parent_name, account_number): + def set_account_number(self, parent_name: str, account_number: str): if parent_name not in self.__account_numbers: self.__account_numbers[parent_name] = [] self.__account_numbers[parent_name].append(account_number) - def set_logged_in_object(self, parent_name, logged_in_object, account_name=None): + def set_logged_in_object(self, parent_name: str, logged_in_object, account_name: str = None): if parent_name not in self.__logged_in_objects: self.__logged_in_objects[parent_name] = {} if account_name is None: @@ -186,7 +205,7 @@ def set_logged_in_object(self, parent_name, logged_in_object, account_name=None) else: self.__logged_in_objects[parent_name][account_name] = logged_in_object - def set_holdings(self, parent_name, account_name, stock, quantity, price): + def set_holdings(self, parent_name: str, account_name: str, stock: str, quantity: float, price: float): quantity = 0 if quantity == "N/A" else quantity price = 0 if price == "N/A" else price if parent_name not in self.__holdings: @@ -199,7 +218,7 @@ def set_holdings(self, parent_name, account_name, stock, quantity, price): "total": round(float(quantity) * float(price), 2), } - def set_account_totals(self, parent_name, account_name, total): + def set_account_totals(self, parent_name: str, account_name: str, total: float): if isinstance(total, str): total = total.replace(",", "").replace("$", "").strip() if parent_name not in self.__account_totals: @@ -209,46 +228,46 @@ def set_account_totals(self, parent_name, account_name, total): self.__account_totals[parent_name].values() ) - def set_account_type(self, parent_name, account_name, account_type): + def set_account_type(self, parent_name: str, account_name: str, account_type: str): if parent_name not in self.__account_types: self.__account_types[parent_name] = {} self.__account_types[parent_name][account_name] = account_type - def get_name(self): + def get_name(self) -> str: return self.__name - def get_account_numbers(self, parent_name=None): + def get_account_numbers(self, parent_name: str = None) -> list or dict: if parent_name is None: return self.__account_numbers return self.__account_numbers.get(parent_name, []) - def get_logged_in_objects(self, parent_name=None, account_name=None): + def get_logged_in_objects(self, parent_name: str = None, account_name: str = None) -> dict: if parent_name is None: return self.__logged_in_objects if account_name is None: return self.__logged_in_objects.get(parent_name, {}) return self.__logged_in_objects.get(parent_name, {}).get(account_name, {}) - def get_holdings(self, parent_name=None, account_name=None): + def get_holdings(self, parent_name: str = None, account_name: str = None) -> dict: if parent_name is None: return self.__holdings if account_name is None: return self.__holdings.get(parent_name, {}) return self.__holdings.get(parent_name, {}).get(account_name, {}) - def get_account_totals(self, parent_name=None, account_name=None): + def get_account_totals(self, parent_name: str = None, account_name: str = None) -> dict: if parent_name is None: return self.__account_totals if account_name is None: return self.__account_totals.get(parent_name, {}) return self.__account_totals.get(parent_name, {}).get(account_name, 0) - def get_account_types(self, parent_name, account_name=None): + def get_account_types(self, parent_name: str, account_name: str = None) -> dict: if account_name is None: return self.__account_types.get(parent_name, {}) return self.__account_types.get(parent_name, {}).get(account_name, "") - def __str__(self): + def __str__(self) -> str: return textwrap.dedent( f""" Brokerage: {self.__name} @@ -394,6 +413,7 @@ async def processTasks(message): except Exception as e: print(f"Error Sending Message: {e}") break + sleep(0.5) def printAndDiscord(message, loop=None): From ad6401c74d42187e8b3b0fdf9f1be2c89a39288f Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 01:48:18 +0000 Subject: [PATCH 2/5] style: format code with Black and isort This commit fixes the style issues introduced in c65be30 according to the output from Black and isort. Details: https://github.com/NelsonDane/auto-rsa/pull/93 --- helperAPI.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/helperAPI.py b/helperAPI.py index 17236b2c..6ce67eb6 100644 --- a/helperAPI.py +++ b/helperAPI.py @@ -182,7 +182,9 @@ def __init__(self, name): self.__account_numbers: dict = ( {} ) # Dictionary of account names and numbers under parent - self.__logged_in_objects: dict = {} # Dictionary of logged in objects under parent + self.__logged_in_objects: dict = ( + {} + ) # Dictionary of logged in objects under parent self.__holdings: dict = {} # Dictionary of holdings under parent self.__account_totals: dict = {} # Dictionary of account totals self.__account_types: dict = {} # Dictionary of account types @@ -197,7 +199,9 @@ def set_account_number(self, parent_name: str, account_number: str): self.__account_numbers[parent_name] = [] self.__account_numbers[parent_name].append(account_number) - def set_logged_in_object(self, parent_name: str, logged_in_object, account_name: str = None): + def set_logged_in_object( + self, parent_name: str, logged_in_object, account_name: str = None + ): if parent_name not in self.__logged_in_objects: self.__logged_in_objects[parent_name] = {} if account_name is None: @@ -205,7 +209,14 @@ def set_logged_in_object(self, parent_name: str, logged_in_object, account_name: else: self.__logged_in_objects[parent_name][account_name] = logged_in_object - def set_holdings(self, parent_name: str, account_name: str, stock: str, quantity: float, price: float): + def set_holdings( + self, + parent_name: str, + account_name: str, + stock: str, + quantity: float, + price: float, + ): quantity = 0 if quantity == "N/A" else quantity price = 0 if price == "N/A" else price if parent_name not in self.__holdings: @@ -241,7 +252,9 @@ def get_account_numbers(self, parent_name: str = None) -> list or dict: return self.__account_numbers return self.__account_numbers.get(parent_name, []) - def get_logged_in_objects(self, parent_name: str = None, account_name: str = None) -> dict: + def get_logged_in_objects( + self, parent_name: str = None, account_name: str = None + ) -> dict: if parent_name is None: return self.__logged_in_objects if account_name is None: @@ -255,7 +268,9 @@ def get_holdings(self, parent_name: str = None, account_name: str = None) -> dic return self.__holdings.get(parent_name, {}) return self.__holdings.get(parent_name, {}).get(account_name, {}) - def get_account_totals(self, parent_name: str = None, account_name: str = None) -> dict: + def get_account_totals( + self, parent_name: str = None, account_name: str = None + ) -> dict: if parent_name is None: return self.__account_totals if account_name is None: From 0cfd5dac3cf3408dc99422fd8ec4bf022bcb674f Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Mon, 9 Oct 2023 21:52:00 -0400 Subject: [PATCH 3/5] shorten messages --- tastyAPI.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tastyAPI.py b/tastyAPI.py index e9859705..422a1dca 100644 --- a/tastyAPI.py +++ b/tastyAPI.py @@ -155,7 +155,8 @@ async def tastytrade_execute(tt_o: Brokerage, orderObj: stockOrder, loop=None): stock_price = 0 # Day trade check # Day trade check - # removing this check until tastytrade api maintainer fixes enhanced_security_check bug + # removing this check until tastytrade api + # maintainer fixes enhanced_security_check bug # cash_balance = float(acct.get_balances(obj).cash_balance) # day_trade_check(obj, acct, cash_balance) if True: @@ -214,7 +215,7 @@ async def tastytrade_execute(tt_o: Brokerage, orderObj: stockOrder, loop=None): message = f"{key} Running in DRY mode. Transaction would've been: {orderObj.get_action()} {orderObj.get_amount()} of {s}" printAndDiscord(message, loop=loop) elif order_status == "Rejected": - # Only want this message if it fails market and limit order. + # Only want this message if it fails both orders. printAndDiscord( f"{key} Error placing order: {placed_order.order.id} on account {acct.account_number}: {order_status}", loop=loop, From b1a0160636cc8da014e2b8ab2f54ff3604de1e1a Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 01:52:37 +0000 Subject: [PATCH 4/5] style: format code with Black and isort This commit fixes the style issues introduced in d9927e5 according to the output from Black and isort. Details: https://github.com/NelsonDane/auto-rsa/pull/93 --- tastyAPI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tastyAPI.py b/tastyAPI.py index 422a1dca..fe46a988 100644 --- a/tastyAPI.py +++ b/tastyAPI.py @@ -155,7 +155,7 @@ async def tastytrade_execute(tt_o: Brokerage, orderObj: stockOrder, loop=None): stock_price = 0 # Day trade check # Day trade check - # removing this check until tastytrade api + # removing this check until tastytrade api # maintainer fixes enhanced_security_check bug # cash_balance = float(acct.get_balances(obj).cash_balance) # day_trade_check(obj, acct, cash_balance) From 1a4b147406d66caf540fc78639d2a2d86161ce4d Mon Sep 17 00:00:00 2001 From: Nelson Dane <47427072+NelsonDane@users.noreply.github.com> Date: Mon, 9 Oct 2023 22:03:22 -0400 Subject: [PATCH 5/5] remove ally support and mentions :( --- .env.example | 6 -- Dockerfile | 1 - README.md | 40 +++----- allyAPI.py | 241 ----------------------------------------------- autoRSA.py | 4 +- helperAPI.py | 2 +- requirements.txt | 1 - testLogin.py | 8 -- 8 files changed, 13 insertions(+), 290 deletions(-) delete mode 100644 allyAPI.py diff --git a/.env.example b/.env.example index 33d2b99d..47bfc5af 100644 --- a/.env.example +++ b/.env.example @@ -15,12 +15,6 @@ DANGER_MODE="false" # at the same brokerage with a comma, then separate account credentials with a colon # BROKER=BROKER_USERNAME:BROKER_PASSWORD,OTHER_BROKER_USERNAME:OTHER_BROKER_PASSWORD -# Ally (CURRENTLY DISABLED) -# ALLY=ALLY_CONSUMER_KEY:ALLY_CONSUMER_SECRET:ALLY_OAUTH_TOKEN:ALLY_OAUTH_SECRET -# ALLY_ACCOUNT_NUMBERS=ALLY_ACCOUNT_NUMBER_1:ALLY_ACCOUNT_NUMBER_2 -ALLY= -ALLY_ACCOUNT_NUMBERS= - # Fidelity # FIDELITY=FIDELITY_USERNAME:FIDELITY_PASSWORD FIDELITY= diff --git a/Dockerfile b/Dockerfile index ae0c35db..bbea096d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,6 @@ RUN playwright install && \ # Grab needed files COPY ./autoRSA.py . -COPY ./allyAPI.py . COPY ./fidelityAPI.py . COPY ./robinhoodAPI.py . COPY ./schwabAPI.py . diff --git a/README.md b/README.md index 2d32cbdd..afd2a090 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ A CLI tool and Discord bot to buy and sell the same amount of stocks across multiple accounts! ## What is RSA? -RSA stands for "Reverse Split Arbitrage." This is a strategy where you buy the same amount of stocks in multiple accounts across multiple brokers right before a stock performs a reverse split. Once the stock splits and your fractional share is rounded up to a full share, you profit! - -This project will allow you to maximize your profits by being able to easily manage multiple accounts across different brokerages, buying and selling as needed. +RSA is a stock trading strategy where buying and selling a stock across multiple brokerages would be very beneficial, hence this project. ## Discord Bot Installation To create your Discord bot and get your `DISCORD_TOKEN`, follow this [guide](guides/discordBot.md). @@ -34,21 +32,21 @@ To buy and sell stocks, use this command: ` ` -For example, to buy 1 STAF in all accounts: +For example, to buy 1 AAPL in all accounts: -`buy 1 STAF all false` +`buy 1 AAPL all false` For a dry run of the above command in Robinhood only: -`buy 1 STAF robinhood true` +`buy 1 AAPL robinhood true` -For a real run on Ally and Robinhood, but not Schwab: +For a real run on Fidelity and Robinhood, but not Schwab: -`buy 1 STAF ally,robinhood not schwab false` +`buy 1 AAPL fidelity,robinhood not schwab false` -For a real run on Ally and Robinhood but not Schwab buying both STAF and AREB: +For a real run on Fidelity and Robinhood but not Schwab buying both AAPL and GOOG: -`buy 1 STAF,AREB ally,robinhood not schwab false` +`buy 1 AAPL,GOOG fidelity,robinhood not schwab false` To check your account holdings: @@ -92,25 +90,7 @@ DISCLAIMER: I am not a financial advisor and not affiliated with any of the brok ## Supported brokerages: -All brokers: separate account credentials with a colon (":"). For example, `ALLY_USERNAME:ALLY_PASSWORD`. Separate multiple logins with the same broker with a comma (","). For example, `ALLY_USERNAME:ALLY_PASSWORD,ALLY_USERNAME2:ALLY_PASSWORD2`. - -### Ally -**Ally has disabled their API, so Ally is currently unsupported.** - -Made using [PyAlly](https://github.com/alienbrett/PyAlly). Go give them a ⭐ - -Required `.env` variables: -- `ALLY_CONSUMER_KEY` -- `ALLY_CONSUMER_SECRET` -- `ALLY_OAUTH_TOKEN` -- `ALLY_OAUTH_SECRET` -- `ALLY_ACCOUNT_NUMBERS` - -`.env` file format: -- `ALLY=ALLY_CONSUMER_KEY:ALLY_CONSUMER_SECRET:ALLY_OAUTH_TOKEN:ALLY_OAUTH_SECRET` -- `ALLY_ACCOUNT_NUMBERS=ALLY_ACCOUNT_NUMBER1:ALLY_ACCOUNT_NUMBER2` - -To get these, follow [these instructions](https://alienbrett.github.io/PyAlly/installing.html#get-the-library). +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`. ### Fidelity Made by yours truly using Selenium (and many hours of web scraping). @@ -177,6 +157,8 @@ Required `.env` variables: - `TASTYTRADE=TASTYTRADE_USERNAME:TASTYTRADE_PASSWORD` ### Maybe future brokerages +#### Ally +Ally disabled their official API, so all Ally packages don't work. I am attempting to reverse engineer their API, which you can track [here](https://github.com/NelsonDane/ally-api). Once I get it working, I will add it to this project. #### Chase I will be signing up for a Chase account soon, and I have heard that it is possible, so I will be looking into it soon. #### Vanguard diff --git a/allyAPI.py b/allyAPI.py deleted file mode 100644 index 758ded4b..00000000 --- a/allyAPI.py +++ /dev/null @@ -1,241 +0,0 @@ -# Nelson Dane -# Ally API - -import os -import traceback - -import ally -import requests -from dotenv import load_dotenv - -from helperAPI import Brokerage, printAndDiscord, printHoldings, stockOrder - - -# Initialize Ally -def ally_init(ALLY_EXTERNAL=None, ALLY_ACCOUNT_NUMBERS_EXTERNAL=None): - # Disable Ally API - print("Ally has disabled their API, so Ally is currently unavailable.") - return None - # Initialize .env file - load_dotenv() - # Import Ally account - if ( - not os.getenv("ALLY") - or not os.getenv("ALLY_ACCOUNT_NUMBERS") - and ALLY_EXTERNAL is None - and ALLY_ACCOUNT_NUMBERS_EXTERNAL is None - ): - print("Ally not found, skipping...") - return None - accounts = ( - os.environ["ALLY"].strip().split(",") - if ALLY_EXTERNAL is None - else ALLY_EXTERNAL.strip().split(",") - ) - account_nbrs_list = ( - os.environ["ALLY_ACCOUNT_NUMBERS"].strip().split(",") - if ALLY_ACCOUNT_NUMBERS_EXTERNAL is None - else ALLY_ACCOUNT_NUMBERS_EXTERNAL.strip().split(",") - ) - params_list = [] - for account in accounts: - name = f"Ally {accounts.index(account) + 1}" - account = account.split(":") - for nbr in account_nbrs_list: - for num in nbr.split(":"): - if len(account) != 4: - print( - f"{name}: Too many parameters for Ally account, please see README.md and .env.example, skipping..." - ) - return None - params = { - "ALLY_CONSUMER_KEY": account[0], - "ALLY_CONSUMER_SECRET": account[1], - "ALLY_OAUTH_TOKEN": account[2], - "ALLY_OAUTH_SECRET": account[3], - "ALLY_ACCOUNT_NBR": num, - } - params_list.append(params) - # Initialize Ally account - ally_obj = Brokerage("Ally") - for account in accounts: - print(f"Logging in to {name}...") - for nbr in account_nbrs_list: - for index, num in enumerate(nbr.split(":")): - try: - a = ally.Ally(params=params_list[index]) - except requests.exceptions.HTTPError as e: - print(f"{name}: Error logging in: {e}") - return None - # Ally needs a different object for each account number - ally_obj.set_logged_in_object(name, a, num) - ally_obj.set_account_number(name, num) - print("Logged in to Ally!") - return ally_obj - - -# Function to get the current account holdings -def ally_holdings(ao: Brokerage, loop=None): - # Disable Ally API - printAndDiscord( - "Ally has disabled their API, so Ally is currently unavailable.", loop - ) - return - for key in ao.get_account_numbers(): - account_numbers = ao.get_account_numbers(key) - for account in account_numbers: - obj: ally.Ally = ao.get_logged_in_objects(key, account) - try: - # Get account holdings - ab = obj.balances() - a_value = ab["accountvalue"].values - for value in a_value: - ao.set_account_totals(key, account, value) - # Print account stock holdings - ah = obj.holdings() - # Test if holdings is empty - if len(ah.index) > 0: - account_symbols = (ah["sym"].values).tolist() - qty = (ah["qty"].values).tolist() - current_price = (ah["marketvalue"].values).tolist() - for i, symbol in enumerate(account_symbols): - ao.set_holdings(key, account, symbol, qty[i], current_price[i]) - except Exception as e: - printAndDiscord(f"{key}: Error getting account holdings: {e}", loop) - print(traceback.format_exc()) - continue - printHoldings(ao, loop) - - -# Function to buy/sell stock on Ally -def ally_transaction( - ao: Brokerage, - orderObj: stockOrder, - loop=None, - RETRY=False, - account_retry=None, -): - print() - print("==============================") - print("Ally") - print("==============================") - print() - # Disable Ally API - printAndDiscord( - "Ally has disabled their API, so Ally is currently unavailable.", loop - ) - return - # Set the action - price = ally.Order.Market() - if isinstance(orderObj.get_price(), (int, float)): - print(f"Limit order at: ${float(orderObj.get_price())}") - price = ally.Order.Limit(limpx=float(orderObj.get_price())) - for s in orderObj.get_stocks(): - for key in ao.get_account_numbers(): - printAndDiscord( - f"{key}: {orderObj.get_action()}ing {orderObj.get_amount()} of {s}", - loop, - ) - for account in ao.get_account_numbers(key): - if not RETRY: - obj: ally.Ally = ao.get_logged_in_objects(key, account) - else: - obj: ally.Ally = ao - account = account_retry - try: - # Create order - o = ally.Order.Order( - buysell=orderObj.get_action(), - symbol=s, - price=price, - time=orderObj.get_time(), - qty=orderObj.get_amount(), - ) - # Print order preview - print(f"{key} {account}: {str(o)}") - # Submit order - o.orderid - if not orderObj.get_dry(): - obj.submit(o, preview=False) - else: - printAndDiscord( - f"{key} {account}: Running in DRY mode. " - + f"Trasaction would've been: {orderObj.get_action()} {orderObj.get_amount()} of {s}", - loop, - ) - # Print order status - if o.orderid: - printAndDiscord( - f"{key} {account}: Order {o.orderid} submitted", loop - ) - else: - printAndDiscord(f"{key} {account}: Order not submitted", loop) - if RETRY: - return - except Exception as e: - ally_call_error = ( - "Error: For your security, certain symbols may only be traded " - + "by speaking to an Ally Invest registered representative. " - + "Please call 1-855-880-2559 if you need further assistance with this order." - ) - if ( - "500 server error: internal server error for url:" - in str(e).lower() - ): - # If selling too soon, then an error is thrown - if orderObj.get_action() == "sell": - printAndDiscord(ally_call_error, loop) - # If the message comes up while buying, then - # try again with a limit order - elif orderObj.get_action() == "buy" and not RETRY: - printAndDiscord( - f"{key} {account}: Error placing market buy, trying again with limit order...", - loop, - ) - # Need to get stock price (compare bid, ask, and last) - try: - # Get stock values - quotes = obj.quote( - s, - fields=["bid", "ask", "last"], - ) - # Add 1 cent to the highest value of the 3 above - new_price = ( - max( - [ - float(quotes["last"][0]), - float(quotes["bid"][0]), - float(quotes["ask"][0]), - ] - ) - ) + 0.01 - # Set new price - orderObj.set_price(new_price) - # Run function again with limit order - ally_transaction( - ao, - orderObj, - loop, - RETRY=True, - account_retry=account, - ) - except Exception as ex: - printAndDiscord( - f"{key} {account}: Failed to place limit order: {ex}", - loop, - ) - else: - printAndDiscord( - f"{key} {account}: Error placing limit order: {e}", - loop, - ) - # If price is not a string then it must've failed a limit order - elif not isinstance(orderObj.get_price(), str): - printAndDiscord( - f"{key} {account}: Error placing limit order: {e}", - loop, - ) - else: - printAndDiscord( - f"{key} {account}: Error submitting order: {e}", loop - ) diff --git a/autoRSA.py b/autoRSA.py index 6239e185..f9a88400 100644 --- a/autoRSA.py +++ b/autoRSA.py @@ -3,7 +3,6 @@ # Import libraries import os -import re import sys import traceback @@ -13,7 +12,6 @@ from dotenv import load_dotenv # Custom API libraries - from allyAPI import * from fidelityAPI import * from helperAPI import killDriver, stockOrder, updater from robinhoodAPI import * @@ -30,7 +28,7 @@ # Global variables -SUPPORTED_BROKERS = ["ally", "fidelity", "robinhood", "schwab", "tastytrade", "tradier"] +SUPPORTED_BROKERS = ["fidelity", "robinhood", "schwab", "tastytrade", "tradier"] DISCORD_BOT = False DOCKER_MODE = False DANGER_MODE = False diff --git a/helperAPI.py b/helperAPI.py index 6ce67eb6..98d9e5a4 100644 --- a/helperAPI.py +++ b/helperAPI.py @@ -27,7 +27,7 @@ def __init__(self): self.__time: str = "day" # Only supports day for now self.__price: str = "market" # Default to market price self.__brokers: list = [] # List of brokerages to use - self.__notbrokers: list = [] # List of brokerages to not use !ally + self.__notbrokers: list = [] # List of brokerages to not use self.__dry: bool = True # Dry run mode self.__holdings: bool = False # Get holdings from enabled brokerages self.__logged_in: dict = {} # Dict of logged in brokerage objects diff --git a/requirements.txt b/requirements.txt index 3bdcb585..47db2013 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ asyncio==3.4.3 discord.py==2.3.2 GitPython==3.1.34 pandas==2.1.0 -pyally==1.1.2 pyotp==2.9.0 python-dotenv==1.0.0 requests==2.31.0 diff --git a/testLogin.py b/testLogin.py index 5348688f..8858b897 100644 --- a/testLogin.py +++ b/testLogin.py @@ -8,7 +8,6 @@ from dotenv import load_dotenv # Custom API libraries -from allyAPI import ally_init from fidelityAPI import fidelity_init from robinhoodAPI import robinhood_init from schwabAPI import schwab_init @@ -28,11 +27,6 @@ print("DISCORD_CHANNEL not found") else: print(f"Discord channel found {os.environ.get('DISCORD_CHANNEL')}") -# Ally -if os.environ.get("ALLY") is None: - print("ALLY not found") -else: - print(f"ALLY found {os.environ.get('ALLY')}") # Fidelity if os.environ.get("FIDELITY") is None: print("FIDELITY not found") @@ -65,8 +59,6 @@ print("Checking Accounts...") print("==========================================================") print() -ally_init() -print() fidelity_init() print() robinhood_init()