diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/StocksGame.iml b/.idea/StocksGame.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/StocksGame.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..f281ec3 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d1e22ec --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f21374d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/__pycache__/picker.cpython-38.pyc b/src/__pycache__/picker.cpython-38.pyc new file mode 100644 index 0000000..d6b0299 Binary files /dev/null and b/src/__pycache__/picker.cpython-38.pyc differ diff --git a/src/__pycache__/stock.cpython-38.pyc b/src/__pycache__/stock.cpython-38.pyc new file mode 100644 index 0000000..2d15eb0 Binary files /dev/null and b/src/__pycache__/stock.cpython-38.pyc differ diff --git a/src/__pycache__/trading.cpython-38.pyc b/src/__pycache__/trading.cpython-38.pyc new file mode 100644 index 0000000..f406a9b Binary files /dev/null and b/src/__pycache__/trading.cpython-38.pyc differ diff --git a/src/main.py b/src/main.py index 6087b9b..2e51336 100644 --- a/src/main.py +++ b/src/main.py @@ -6,21 +6,4 @@ if __name__ == "__main__": trader = TradingBot() while True: _in = input("> ") - words = _in.split(" ") - if len(words) == 0: - continue - cmd = words.pop(0) - args = [] - kwargs = {} - for w in words: - components = w.split("=") - if len(components) == 1: - args.append(w) - elif len(components) == 2: - kwargs[components[0]] = components[1] - else: - print("Invalid parameter: " + w) - continue - - trader.interpret_command(cmd, *args, **kwargs) - # trader.buy("GOOG", 1) + trader.interpret_command(_in.lower()) diff --git a/src/picker.py b/src/picker.py new file mode 100644 index 0000000..2aeca9d --- /dev/null +++ b/src/picker.py @@ -0,0 +1,79 @@ +import mechanicalsoup +from stock import StockList + + +class StockAnalyzer(mechanicalsoup.StatefulBrowser): + yahoo_52wk_low = "https://finance.yahoo.com/u/yahoo-finance/watchlists/fiftytwo-wk-low" + yahoo_52wk_high = "https://finance.yahoo.com/u/yahoo-finance/watchlists/fiftytwo-wk-high" + yahoo_most_added = "https://finance.yahoo.com/u/yahoo-finance/watchlists/most-added" + yahoo_active_spacs = "https://finance.yahoo.com/u/yahoo-finance/watchlists/most-active-spacs" + yahoo_covid = "https://finance.yahoo.com/u/trea/watchlists/the-fight-against-covid19" + yahoo_small_cap = "https://finance.yahoo.com/u/yahoo-finance/watchlists/most-active-small-cap-stocks" + + def __init__(self): + super().__init__(user_agent="Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0") + + def get_prediction(self, symbol: str): + status = self.open("https://finance.yahoo.com/quote/" + symbol) + if not status.ok: + print("Invalid symbol: " + symbol) + return "invalid" + element = self.page.find("div", {"Fw(b) Fl(end)--m Fz(s) C($primaryColor"}) + if element is None: + return "same" + value = element.get_text() + if value is None: + return "same" + if value == "Near Fair Value": + return "same" + if value == "Overvalued": + return "down" + if value == "Undervalued": + return "up" + + def grab_yahoo_watchlist(self, link): + self.open(link) + table = self.page.find("table", {"class": "cwl-symbols W(100%)"}) + data = [] + for item in table.find_all("td"): + data.append(item.get_text()) + count = len(data) / 9 + companies = StockList() + for i in range(int(count)): + start = i * 9 + # WHY TF ARE THERE 2 SYMBOLS FOR GOOGLE????? + if data[start + 1] == "GOOGL": + data[start + 1] = "GOOG" + company = [ + data[start + 0], + data[start + 1], + data[start + 2], + data[start + 3], + data[start + 4], + data[start + 8], + 1, + self.get_prediction(data[start + 0]) + ] + companies.add_stock(company) + return companies + + def recommend_buy(self, budget): + print("Scanning largest 52 week losses...") + companies = self.grab_yahoo_watchlist(self.yahoo_52wk_low) + print("Scanning largest 52 week gains...") + companies.concat(self.grab_yahoo_watchlist(self.yahoo_52wk_high), False) + print("Scanning most watched stocks...") + companies.concat(self.grab_yahoo_watchlist(self.yahoo_most_added), False) + print("Scanning most active SPAC's...") + companies.concat(self.grab_yahoo_watchlist(self.yahoo_active_spacs), False) + print("Scanning COVID-19 related stocks...") + companies.concat(self.grab_yahoo_watchlist(self.yahoo_covid), False) + print("Scanning small market cap stocks...") + companies.concat(self.grab_yahoo_watchlist(self.yahoo_small_cap), False) + print("Sorting results...") + companies.keep_recommended() + print("Budgeting...") + companies.budget(int(budget)) + return companies + + diff --git a/src/stock.py b/src/stock.py new file mode 100644 index 0000000..f9876e9 --- /dev/null +++ b/src/stock.py @@ -0,0 +1,119 @@ +class StockList: + def __init__(self): + self.symbol = [] + self.name = [] + self.price = [] + self.change_absolute = [] + self.change_relative = [] + self.market_cap = [] + self.amount = [] + self.prediction = [] + + def sanity_check(self): + # debug + assert len(self.symbol) == len(self.name) + assert len(self.name) == len(self.price) + assert len(self.price) == len(self.change_absolute) + assert len(self.change_absolute) == len(self.change_relative) + assert len(self.change_relative) == len(self.market_cap) + assert len(self.market_cap) == len(self.amount) + assert len(self.amount) == len(self.prediction) + + def count(self): + self.sanity_check() + return len(self.symbol) + + def total(self): + total = 0 + for i in range(self.count()): + total += self.price[i] * self.amount[i] + return total + + def add_stock(self, data, allow_duplicates=True): + dupe = data[0] in self.symbol + if dupe and not allow_duplicates: + index = self.symbol.index(data[0]) + self.amount[index] += int(data[6]) + return + self.symbol += [str(data[0])] + self.name += [str(data[1])] + self.price += [float(data[2])] + self.change_absolute += [str(data[3])] + self.change_relative += [str(data[4])] + self.market_cap += [str(data[5])] + self.amount += [int(data[6])] + self.prediction += [str(data[7])] + self.sanity_check() + + def concat(self, stocks, allow_duplicates=True): + for i in range(stocks.count()): + self.add_stock(stocks.get_stock(i), allow_duplicates) + + def remove_stock(self, index): + if self.count() <= index: + print("Out of range") + return + self.symbol.pop(index) + self.name.pop(index) + self.price.pop(index) + self.change_absolute.pop(index) + self.change_relative.pop(index) + self.market_cap.pop(index) + self.amount.pop(index) + self.prediction.pop(index) + + def get_stock(self, index): + return [ + self.symbol[index], + self.name[index], + self.price[index], + self.change_absolute[index], + self.change_relative[index], + self.market_cap[index], + self.amount[index], + self.prediction[index] + ] + + def most_expensive(self): + index = 0 + highest = 0 + for i in range(self.count()): + if self.price[i] >= highest: + index = i + highest = self.price[i] + return index + + def least_expensive(self): + smallest = 9999999999 + index = 0 + for s in range(self.count()): + if self.price[s] <= smallest: + smallest = self.price[s] + index = s + return index + + def budget(self, budget): + total = self.total() + while total > budget: + i = self.most_expensive() + self.remove_stock(i) + total = self.total() + print(self.name) + if self.count() == 0: + print("No stocks found") + return + while total + self.price[self.least_expensive()] < budget: + for stock in range(self.count()): + if total + self.price[stock] <= budget: + self.amount[stock] += 1 + total = self.total() + + def keep_recommended(self): + i = 0 + size = self.count() + while i < size: + while i < size and self.prediction[i] != "up": + self.remove_stock(i) + size = self.count() + i += 1 + diff --git a/src/trading.py b/src/trading.py index a27a5b3..1bc4cc2 100644 --- a/src/trading.py +++ b/src/trading.py @@ -1,113 +1,6 @@ import mechanicalsoup import getpass -import bs4 -import selenium - - -def _convert_symbol(dec: str): - mult = 1 - if dec.find("M") != -1: - mult = 1000000 - elif dec.find("B") != -1: - mult = 1000000000 - dec = dec.replace("M", "") - dec = dec.replace("B", "") - dec = dec.replace(",", "") - return float(dec) * mult - - -def _convert_decimal(dec: str): - dec = dec.replace("%", "") - return float(dec) / 100 - - -def _sum_total(stocks: list[dict]): - ret = 0 - for s in stocks: - ret += s["price"] * s["amount"] - return ret - - -def _most_expensive(stocks: list[dict]): - if len(stocks) == 0: - return 0 - largest = 0 - index = 0 - for s in range(len(stocks)): - if stocks[s]["price"] >= largest: - largest = stocks[s]["price"] - index = s - return index - - -def _least_expensive(stocks: list[dict]): - if len(stocks) == 0: - return 0 - smallest = 9999999999 - index = 0 - for s in range(len(stocks)): - if stocks[s]["price"] <= smallest: - smallest = stocks[s]["price"] - index = s - return index - - -class StockAnalyzer(mechanicalsoup.StatefulBrowser): - def __init__(self): - super().__init__(user_agent="Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0") - - def recommend_buy(self, budget): - budget = float(budget) - data = [] - self.open("https://finance.yahoo.com/u/yahoo-finance/watchlists/fiftytwo-wk-low") - table = self.page.find("table", {"class": "cwl-symbols W(100%)"}) - for item in table.find_all("td"): - data.append(item.get_text()) - count = len(data) / 9 - companies = [] - cart = [] - for i in range(int(count)): - start = i * 9 - company = { - "symbol": data[start + 0], - "name": data[start + 1], - "price": float(data[start + 2]), - "change-absolute": float(data[start + 3]), - "change-relative": _convert_decimal(data[start + 4]), - "market-cap": _convert_symbol(data[start + 8]), - "amount": 1 - } - if company["price"] >= budget / 2: - continue - companies.append(company) - prediction = self.get_prediction(company["symbol"]) - if prediction == "up": - cart.append(company) - - total = _sum_total(cart) - while total > budget: - i = _most_expensive(cart) - cart.pop(i) - total = _sum_total(cart) - while total + cart[_least_expensive(cart)]["price"] < budget: - for stock in cart: - if total + stock["price"] <= budget: - stock["amount"] += 1 - total = _sum_total(cart) - return cart - - def get_prediction(self, symbol: str): - status = self.open("https://finance.yahoo.com/quote/" + symbol) - if not status.ok: - print("Invalid symbol: " + symbol) - return "invalid" - value = self.page.find("div", {"Fw(b) Fl(end)--m Fz(s) C($primaryColor"}).get_text() - if value == "Near Fair Value": - return "same" - if value == "Overvalued": - return "down" - if value == "Undervalued": - return "up" +from picker import StockAnalyzer class TradingBot(mechanicalsoup.StatefulBrowser): @@ -115,7 +8,6 @@ class TradingBot(mechanicalsoup.StatefulBrowser): super().__init__(user_agent="Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0") self.logged_in = False self.commands = { - "help": self.help, "quit": quit, "login": self.login, "buy": self.buy, @@ -124,37 +16,26 @@ class TradingBot(mechanicalsoup.StatefulBrowser): } self.analyzer = StockAnalyzer() - def help(self): - print("Unimplemented") - - def interpret_command(self, cmd, *args, **kwargs): - cmd = cmd.lower() + def interpret_command(self, cmd): if cmd not in self.commands.keys(): print("Invalid command " + cmd + ", type help for more info") return - return self.commands[cmd](*args, **kwargs) + return self.commands[cmd]() - def login(self, *args, **kwargs): + def login(self, _username="", _password=""): # Navigate to login page self.open("https://www.howthemarketworks.com/login") - self.select_form('form[action="/login"]') # Get login info - _username = args[0] if len(args) > 0 else "" - _password = args[1] if len(args) > 1 else "" - for key in kwargs.keys(): - key = key.lower() - if key == "username" or key == "user": - _username = kwargs[key] - if key == "password" or key == "pass": - _password = kwargs[key] if _username == "": _username = input("Username: ") if _password == "": _password = getpass.getpass(prompt="Password: ") # Submit form and get result + self.select_form('form[action="/login"]') self.__setitem__("UserName", _username) self.__setitem__("Password", _password) result = self.submit_selected() + self.logged_in = result.ok and self.url == "https://www.howthemarketworks.com/accounting/dashboard" if self.logged_in: print("Log-in successful") @@ -162,32 +43,34 @@ class TradingBot(mechanicalsoup.StatefulBrowser): print("Log-in failed") return result - def buy(self, *args, **kwargs): + def buy(self, symbol="", quantity="", t="", stop=0): if not self.logged_in: print("Not logged in") return None - - symbol = args[0] if len(args) > 0 else "" - quantity = args[1] if len(args) > 1 else "" - for key in kwargs.keys(): - key = key.lower() - if key == "sym" or key == "symbol": - symbol = kwargs[key] - if key == "quantity" or key == "q": - quantity = kwargs[key] + stop = float(stop) if symbol == "": symbol = input("Stock Symbol: ") if quantity == "": quantity = input("Quantity: ") + if t == "": + trailing = input("Make order trailing stop? Y/N - ") + if trailing: + t = "Trailing Stop %" + stop = input("What percent of price to stop? __/100 - ") + else: + t = "Market" self.open("https://www.howthemarketworks.com/trading/equities") self.select_form() self.__setitem__("Symbol", symbol) self.__setitem__("Quantity", quantity) + self.__setitem__("OrderType", t) + if t == "Trailing Stop %": + self.__setitem__("Price", stop) result = self.submit_selected() if result.ok: - print("Successfully bought " + str(quantity) + " shares of " + symbol) + print("Successfully bought " + str(quantity) + " shares of " + symbol + " with type " + t) else: print("Failed to purchase " + symbol + ". Check that the symbol is correct and funds are sufficient") return result @@ -199,23 +82,35 @@ class TradingBot(mechanicalsoup.StatefulBrowser): self.open("https://www.howthemarketworks.com/accounting/accountbalance") self.launch_browser() - def autobuy(self, budget=1000): + def autobuy(self, budget=0): + if budget == 0: + budget = input("Budget for autobuy: ") print("Analyzing market, this may take time") stocks = self.analyzer.recommend_buy(budget) - total = _sum_total(stocks) + total = stocks.total() symbols = [] print("The following stocks have been selected:") - for s in stocks: - symbols.append(s["symbol"]) - print(s["name"] + " (" + s["symbol"] + ") ---- " + str(s["amount"]) + " share(s) totalling: $" + str(round(s["amount"] * s["price"], 2))) + for s in range(stocks.count()): + symbols.append(stocks.symbol[s]) + print(stocks.name[s] + " (" + stocks.symbol[s] + ") ---- " + str(stocks.amount[s]) + + " share(s) totalling: $" + str(round(stocks.amount[s] * stocks.price[s], 2))) print("Purchase will total $" + str(round(total, 2))) + order_type = "Market" + percent = 0 + trailing = input("Add trailing stop? Y/N - ") + if trailing.lower() == "y": + order_type = "Trailing Stop %" + percent = input("What percent to put the stop? __/100 - ") print("Make sure you have sufficient funds before continuing") decision = input("Finalize purchase? Y/N - ") if decision.lower() != "y": print("Cancelled order") return - for s in stocks: - self.buy(s["symbol"], s["amount"]) + for s in range(stocks.count()): + if order_type == "Market": + self.buy(stocks.symbol[s], str(stocks.amount[s]), order_type) + else: + self.buy(stocks.symbol[s], str(stocks.amount[s]), order_type, percent) if __name__ == "__main__":