From 65164c902bad88a5329671bc5d641c336378ce04 Mon Sep 17 00:00:00 2001 From: StephanAkkerman Date: Sat, 23 Dec 2023 12:30:25 +0100 Subject: [PATCH] Working on #469 Still need to fix the stock message + worth values --- src/cogs/loops/assets.py | 159 ++++++++++++++++++++++--------------- src/cogs/loops/listings.py | 1 + src/util/cg_data.py | 7 +- src/util/exchange_data.py | 30 +++++-- src/util/trades_msg.py | 75 ++++++++--------- src/util/vars.py | 3 +- 6 files changed, 164 insertions(+), 111 deletions(-) diff --git a/src/cogs/loops/assets.py b/src/cogs/loops/assets.py index 1635f2a8..47f11339 100644 --- a/src/cogs/loops/assets.py +++ b/src/cogs/loops/assets.py @@ -29,13 +29,9 @@ class Assets(commands.Cog): You can enabled / disable it in config under ["LOOPS"]["ASSETS"]. """ - def __init__( - self, bot: commands.Bot, db: pd.DataFrame = util.vars.portfolio_db - ) -> None: + def __init__(self, bot: commands.Bot) -> None: self.bot = bot - - # Refresh assets - asyncio.create_task(self.assets(db)) + self.assets.start() async def usd_value(self, asset: str, exchange: str) -> tuple[float, str]: """ @@ -67,7 +63,8 @@ async def usd_value(self, asset: str, exchange: str) -> tuple[float, str]: return usd_val, change - async def assets(self, portfolio_db: pd.DataFrame) -> None: + @loop(hours=1) + async def assets(self) -> None: """ Only do this function at startup and if a new portfolio has been added. Checks the account balances of accounts saved in portfolio db, then updates the assets db. @@ -81,45 +78,74 @@ async def assets(self, portfolio_db: pd.DataFrame) -> None: ------- None """ - - if portfolio_db.equals(util.vars.portfolio_db): - # Drop all crypto assets - if not util.vars.assets_db.empty: - crypto_rows = util.vars.assets_db.index[ - util.vars.assets_db["exchange"] != "stock" - ].tolist() - assets_db = util.vars.assets_db.drop(index=crypto_rows) - else: - assets_db = pd.DataFrame( - columns=["asset", "buying_price", "owned", "exchange", "id", "user"] - ) + assets_db_columns = { + "asset": str, + "buying_price": float, + "owned": float, + "exchange": str, + "id": np.int64, + "user": str, + "worth": float, + "price": float, + "change": float, + } + + if util.vars.portfolio_db.empty: + print("No portfolios in the database.") + return + + # Drop all crypto assets, so we can update them + if not util.vars.assets_db.empty: + crypto_rows = util.vars.assets_db.index[ + util.vars.assets_db["exchange"] != "stock" + ].tolist() + assets_db = util.vars.assets_db.drop(index=crypto_rows) else: - # Add it to the old assets db, since this call is for a specific person - assets_db = util.vars.assets_db + # Create a new database + assets_db = pd.DataFrame(columns=list(assets_db_columns.keys())) - if not portfolio_db.empty: - for _, row in portfolio_db.iterrows(): - # Add this data to the assets.db database - exch_data = await get_data(row) - assets_db = pd.concat([assets_db, exch_data], ignore_index=True) + # Get the assets of each user + for _, row in util.vars.portfolio_db.iterrows(): + # Add this data to the assets db + exch_data = await get_data(row) + assets_db = pd.concat([assets_db, exch_data], ignore_index=True) # Ensure that the db knows the right types - assets_db = assets_db.astype( - { - "asset": str, - "buying_price": float, - "owned": float, - "exchange": str, - "id": np.int64, - "user": str, - } - ) + assets_db = assets_db.astype(assets_db_columns) # Update the assets db update_db(assets_db, "assets") util.vars.assets_db = assets_db - self.post_assets.start() + # Post the assets + await self.post_assets() + + async def update_prices_and_changes(self, new_df): + # Filter DataFrame to only include rows where exchange is "Stock" + print(new_df) + stock_df = new_df[new_df["exchange"] == "Stock"] + + # Asynchronously get price and change for each asset + async def get_price_change(row): + price, change = await self.usd_value(row["asset"], row["exchange"]) + return { + "price": 0 if price is None else round(price, 2), + "change": 0 if change is None else change, + } + + # Using asyncio.gather to run all async operations concurrently + results = await asyncio.gather( + *(get_price_change(row) for _, row in stock_df.iterrows()) + ) + print(stock_df) + print(results) + + # Update the DataFrame with the results + for i, (index, row) in enumerate(stock_df.iterrows()): + new_df.at[index, "price"] = results[i]["price"] + new_df.at[index, "change"] = results[i]["change"] + + return new_df async def format_exchange( self, @@ -152,40 +178,38 @@ async def format_exchange( # Necessary to prevent panda warnings new_df = exchange_df.copy() - # Get the price of the assets - prices = [] - changes = [] - for _, row in new_df.iterrows(): - price, change = await self.usd_value(row["asset"], exchange) + # Usage + new_df = await self.update_prices_and_changes(new_df) - if price is None: - price = 0 - if change is None: - change = 0 - - prices.append(round(price, 2)) - # Add without emoji - changes.append(change) + # Set the types (again) + new_df = new_df.astype( + { + "asset": str, + "buying_price": float, + "owned": float, + "exchange": str, + "id": np.int64, + "user": str, + "worth": float, + "price": float, + "change": float, + } + ) - new_df["price"] = prices - new_df["change"] = changes + # Format the price change + new_df["change"] = new_df["change"].apply(lambda x: format_change(x)) # Format price and change new_df["price_change"] = ( - "$" + new_df["price"].astype(str) + " (" + new_df["change"] + ")" + "$" + + new_df["price"].astype(str) + + " (" + + new_df["change"].astype(str) + + ")" ) - # Ensure that 'owned' column is of a numeric type - new_df["owned"] = pd.to_numeric(new_df["owned"], errors="coerce") - - # Calculate the most recent worth - new_df["worth"] = pd.Series(prices) * new_df["owned"] - - # Round it to 2 decimals - new_df = new_df.round({"worth": 2}) - - # Drop it if it's worth less than 1$ - new_df = new_df.drop(new_df[new_df.worth < 1].index) + # Fill NaN values of worth + new_df["worth"] = new_df["worth"].fillna(0) # Set buying price to float new_df["buying_price"] = new_df["buying_price"].astype(float) @@ -207,7 +231,11 @@ async def format_exchange( new_df = new_df.sort_values(by=["worth"], ascending=False) new_df["worth"] = ( - "$" + new_df["worth"].astype(str) + " (" + new_df["worth_change"] + ")" + "$" + + new_df["worth"].astype(str) + + " (" + + new_df["worth_change"].astype(str) + + ")" ) # Create the list of string values @@ -229,7 +257,6 @@ async def format_exchange( return e - @loop(hours=1) async def post_assets(self) -> None: """ Posts the assets of the users that added their portfolio. diff --git a/src/cogs/loops/listings.py b/src/cogs/loops/listings.py index b456fa04..6592d644 100644 --- a/src/cogs/loops/listings.py +++ b/src/cogs/loops/listings.py @@ -142,6 +142,7 @@ async def new_listings(self) -> None: new_symbols = await self.get_symbols(exchange) new_listings = [] + delistings = [] if self.old_symbols[exchange] == []: await self.set_old_symbols() diff --git a/src/util/cg_data.py b/src/util/cg_data.py index 9b1f567a..15727744 100644 --- a/src/util/cg_data.py +++ b/src/util/cg_data.py @@ -36,7 +36,8 @@ def get_crypto_info(ids): best_vol = volume id = symbol coin_dict = coin_info - except Exception: + except Exception as e: + print("Error getting coin info for", symbol, "Error:", e) pass else: @@ -44,7 +45,8 @@ def get_crypto_info(ids): # Try in case the CoinGecko API does not work try: coin_dict = cg.get_coin_by_id(id) - except Exception: + except Exception as e: + print("Error getting coin info for", id, "Error:", e) return None, None return coin_dict, id @@ -151,6 +153,7 @@ async def get_coin_info( coin_dict = None if ticker in util.vars.cg_db["symbol"].values: # Check coin by symbol, i.e. "BTC" + print(ticker) coin_dict, id = get_crypto_info( util.vars.cg_db[util.vars.cg_db["symbol"] == ticker]["id"] ) diff --git a/src/util/exchange_data.py b/src/util/exchange_data.py index 90c57871..0a62676a 100644 --- a/src/util/exchange_data.py +++ b/src/util/exchange_data.py @@ -28,9 +28,11 @@ async def get_data(row) -> pd.DataFrame: owned = [] for symbol, amount in balances.items(): - usd_val = await get_usd_price(exchange, symbol) + usd_val, percentage = await get_usd_price(exchange, symbol) worth = amount * usd_val + # Add price change + if worth < 5: continue @@ -45,11 +47,15 @@ async def get_data(row) -> pd.DataFrame: "exchange": exchange.id, "id": row["id"], "user": row["user"], + "worth": round(worth, 2), + "price": usd_val, + "change": percentage, } ) df = pd.DataFrame(owned) + # Se tthe types if not df.empty: df = df.astype( { @@ -59,6 +65,9 @@ async def get_data(row) -> pd.DataFrame: "exchange": str, "id": np.int64, "user": str, + "worth": float, + "price": float, + "change": float, } ) @@ -81,17 +90,23 @@ async def get_balance(exchange) -> dict: return {} -async def get_usd_price(exchange, symbol) -> float: +async def get_usd_price(exchange, symbol: str) -> tuple[float, float]: """ Returns the price of the symbol in USD Symbol must be in the format 'BTC/USDT' """ + exchange_price = 0 + exchange_change = 0 + if symbol not in stables: for usd in stables: try: price = await exchange.fetchTicker(f"{symbol}/{usd}") if price != 0: - return float(price["last"]) + if "last" in price: + exchange_price = float(price["last"]) + if "percentage" in price: + exchange_change = float(price["percentage"]) except ccxt.BadSymbol: continue except ccxt.ExchangeError as e: @@ -101,11 +116,14 @@ async def get_usd_price(exchange, symbol) -> float: else: try: price = await exchange.fetchTicker(symbol + "/DAI") - return float(price["last"]) + if "last" in price: + exchange_price = float(price["last"]) + if "percentage" in price: + exchange_change = float(price["percentage"]) except ccxt.BadSymbol: - return 1 + return 1, 0 - return 0 + return exchange_price, exchange_change async def get_buying_price(exchange, symbol, full_sym: bool = False) -> float: diff --git a/src/util/trades_msg.py b/src/util/trades_msg.py index e30b0d79..3cca41e5 100644 --- a/src/util/trades_msg.py +++ b/src/util/trades_msg.py @@ -15,11 +15,13 @@ from util.formatting import format_change -async def on_msg(msg: list, - exchange : ccxt.pro.Exchange, - trades_channel : discord.TextChannel, - row : pd.Series, - user : discord.User) -> None: +async def on_msg( + msg: list, + exchange: ccxt.pro.Exchange, + trades_channel: discord.TextChannel, + row: pd.Series, + user: discord.User, +) -> None: """ This function is used to handle the incoming messages from the binance websocket. @@ -32,28 +34,27 @@ async def on_msg(msg: list, ------- None """ - - + msg = msg[0] - sym = msg['symbol'] #BNB/USDT - orderType = msg['type'] # market, limit, stop, stop limit - side = msg['side'] # buy, sell - price = float(round(msg['price'],4)) - amount = float(round(msg['amount'],4)) - cost = float(round(msg['cost'],4)) # If /USDT, then this is the USD value - + sym = msg["symbol"] # BNB/USDT + orderType = msg["type"] # market, limit, stop, stop limit + side = msg["side"] # buy, sell + price = float(round(msg["price"], 4)) + amount = float(round(msg["amount"], 4)) + cost = float(round(msg["cost"], 4)) # If /USDT, then this is the USD value + # Get the value in USD usd = price - base = sym.split('/')[0] - quote = sym.split('/')[1] + base = sym.split("/")[0] + quote = sym.split("/")[1] if quote not in stables: - usd = await get_usd_price(exchange, base) - + usd, change = await get_usd_price(exchange, base) + # Get profit / loss if it is a sell buying_price = None - if side == 'sell': + if side == "sell": buying_price = await get_buying_price(exchange, sym, True) - + # Send it in the discord channel await util.trades_msg.trades_msg( exchange.id, @@ -74,18 +75,19 @@ async def on_msg(msg: list, # Drop all rows for this user and exchange updated_assets_db = assets_db.drop( assets_db[ - (assets_db["id"] == row['id']) & (assets_db["exchange"] == exchange.id) + (assets_db["id"] == row["id"]) & (assets_db["exchange"] == exchange.id) ].index ) - assets_db = pd.concat( - [updated_assets_db, await get_data(row)] - ).reset_index(drop=True) + assets_db = pd.concat([updated_assets_db, await get_data(row)]).reset_index( + drop=True + ) update_db(assets_db, "assets") util.vars.assets_db = assets_db # Maybe post the updated assets of this user as well + async def trades_msg( exchange: str, channel: discord.TextChannel, @@ -96,7 +98,7 @@ async def trades_msg( price: float, quantity: float, usd: float, - buying_price : float = None, + buying_price: float = None, ) -> None: """ Formats the Discord embed that will be send to the dedicated trades channel. @@ -126,11 +128,13 @@ async def trades_msg( ------- None """ - + # Same as in formatting.py if exchange == "binance": color = 0xF0B90B - icon_url = "https://upload.wikimedia.org/wikipedia/commons/5/57/Binance_Logo.png" + icon_url = ( + "https://upload.wikimedia.org/wikipedia/commons/5/57/Binance_Logo.png" + ) url = f"https://www.binance.com/en/trade/{symbol}" elif exchange == "kucoin": color = 0x24AE8F @@ -138,7 +142,9 @@ async def trades_msg( url = f"https://www.kucoin.com/trade/{symbol}" else: color = 0x720E9E - icon_url = "https://s.yimg.com/cv/apiv2/myc/finance/Finance_icon_0919_250x252.png" + icon_url = ( + "https://s.yimg.com/cv/apiv2/myc/finance/Finance_icon_0919_250x252.png" + ) url = f"https://finance.yahoo.com/quote/{symbol}" e = discord.Embed( @@ -158,15 +164,15 @@ async def trades_msg( value=f"${price}" if symbol.endswith(tuple(stables)) else price, inline=True, ) - + if buying_price and buying_price != 0: price_change = price - buying_price - + if price_change != 0: percent_change = round((price_change / buying_price) * 100, 2) else: percent_change = 0 - + percent_change = format_change(percent_change) profit_loss = f"${round(price_change * quantity, 2)} ({percent_change})" @@ -186,13 +192,10 @@ async def trades_msg( inline=True, ) - e.set_footer( - text="\u200b", - icon_url=icon_url - ) + e.set_footer(text="\u200b", icon_url=icon_url) await channel.send(embed=e) # Tag the person if orderType.upper() != "MARKET": - await channel.send(f"<@{user.id}>") \ No newline at end of file + await channel.send(f"<@{user.id}>") diff --git a/src/util/vars.py b/src/util/vars.py index ccd35c79..429cfef6 100644 --- a/src/util/vars.py +++ b/src/util/vars.py @@ -53,7 +53,7 @@ "reddit": {"color": 0xFF3F18, "icon": icon_url + "reddit.png"}, "nasdaqtrader": {"color": 0x0996C7, "icon": icon_url + "nasdaqtrader.png"}, "stocktwits": {"color": 0xFFFFFF, "icon": icon_url + "stocktwits.png"}, - "cryptocraft": {"color": 0x634c7b, "icon": icon_url + "cryptocraft.png"}, + "cryptocraft": {"color": 0x634C7B, "icon": icon_url + "cryptocraft.png"}, } # Stable coins @@ -73,6 +73,7 @@ "FEI", "USD", "USDTPERP", + "EUR", ] # Init global database vars