diff --git a/.gitignore b/.gitignore index 9d7dcf48..55a9e47c 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,6 @@ quants_lab/optimizations/* quants_lab/strategy/experiments/* # Master bot template user-added configs -hummingbot_files/templates/master_bot_conf/conf/* \ No newline at end of file +hummingbot_files/templates/master_bot_conf/conf/* + +**/.DS_Store \ No newline at end of file diff --git a/pages/bot_orchestration/app.py b/pages/bot_orchestration/app.py index 8fbd1079..99198c81 100644 --- a/pages/bot_orchestration/app.py +++ b/pages/bot_orchestration/app.py @@ -17,6 +17,10 @@ from ui_components.launch_broker_card import LaunchBrokerCard from utils.st_utils import initialize_st_page +CARD_WIDTH = 6 +CARD_HEIGHT = 3 +NUM_CARD_COLS = 2 + initialize_st_page(title="Instances", icon="🦅", initial_sidebar_state="collapsed") if "is_broker_running" not in st.session_state: @@ -85,10 +89,6 @@ def update_containers_info(docker_manager): docker_manager = DockerManager() -CARD_WIDTH = 6 -CARD_HEIGHT = 3 -NUM_CARD_COLS = 2 - if not docker_manager.is_docker_running(): st.warning("Docker is not running. Please start Docker and refresh the page.") st.stop() diff --git a/ui_components/bot_performance_card.py b/ui_components/bot_performance_card.py index e077c189..2818d833 100644 --- a/ui_components/bot_performance_card.py +++ b/ui_components/bot_performance_card.py @@ -4,7 +4,24 @@ import streamlit as st import time from utils.os_utils import get_python_files_from_directory, get_yml_files_from_directory +from utils.status_parser import StatusParser +import pandas as pd +import datetime +TRADES_TO_SHOW = 5 +WIDE_COL_WIDTH = 180 +MEDIUM_COL_WIDTH = 150 + +def time_ago(ts): + now_utc = datetime.datetime.now(datetime.timezone.utc) + seconds_since_epoch_utc = now_utc.timestamp() + delta = round(seconds_since_epoch_utc - ts / 1000) + if delta < 60: + return f"{delta}s ago" + if delta < 3600: + return f"{delta // 60}m ago" + else: + return f"{delta // 3600}h ago" class BotPerformanceCard(Dashboard.Item): @@ -32,10 +49,7 @@ def __call__(self, bot_config: dict): scripts = [file.split("/")[-1] for file in get_python_files_from_directory(scripts_directory)] strategies = [file.split("/")[-1] for file in get_yml_files_from_directory(strategies_directory)] if bot_config["selected_strategy"] is None: - if len(scripts): - st.session_state.active_bots[bot_name]["selected_strategy"] = scripts[0] - elif len(strategies): - st.session_state.active_bots[bot_name]["selected_strategy"] = strategies[0] + st.session_state.active_bots[bot_name]["selected_strategy"] = "" with mui.Card(key=self._key, sx={"display": "flex", "flexDirection": "column", "borderRadius": 2, "overflow": "auto"}, @@ -52,14 +66,90 @@ def __call__(self, bot_config: dict): ) if bot_config["is_running"]: with mui.CardContent(sx={"flex": 1}): - with mui.Paper(elevation=2, sx={"padding": 2, "marginBottom": 2}): - mui.Typography("Status") - mui.Typography(bot_config["status"], sx={"fontSize": "0.75rem"}) - with mui.Accordion(sx={"padding": 2, "marginBottom": 2}): - with mui.AccordionSummary(expandIcon="▼"): - mui.Typography("Trades" + "(" + str(len(bot_config["trades"])) + ")") - with mui.AccordionDetails(): - mui.Typography(str(bot_config["trades"]), sx={"fontSize": "0.75rem"}) + # Balances Table + mui.Typography("Balances", variant="h6") + + # # Convert list of dictionaries to DataFrame + balances = StatusParser(bot_config["status"], type="balances").parse() + if balances != "No balances": + df_balances = pd.DataFrame(balances) + balances_rows = df_balances.to_dict(orient='records') + balances_cols = [{'field': col, 'headerName': col} for col in df_balances.columns] + + for column in balances_cols: + # Customize width for 'exchange' column + if column['field'] == 'Exchange': + column['width'] = WIDE_COL_WIDTH + mui.DataGrid(rows=balances_rows, + columns=balances_cols, + autoHeight=True, + density="compact", + disableColumnSelector=True, + hideFooter=True, + initialState={"columns": {"columnVisibilityModel": {"id": False}}}) + else: + mui.Typography(str(balances), sx={"fontSize": "0.75rem"}) + + # Active Orders Table + mui.Typography("Active Orders", variant="h6", sx={"marginTop": 2}) + + # Convert list of dictionaries to DataFrame + orders = StatusParser(bot_config["status"], type="orders").parse() + if orders != "No active maker orders" or "No matching string": + df_orders = pd.DataFrame(orders) + orders_rows = df_orders.to_dict(orient='records') + orders_cols = [{'field': col, 'headerName': col} for col in df_orders.columns] + + for column in orders_cols: + # Customize width for 'exchange' column + if column['field'] == 'Exchange': + column['width'] = WIDE_COL_WIDTH + # Customize width for column + if column['field'] == 'Price': + column['width'] = MEDIUM_COL_WIDTH + + mui.DataGrid(rows=orders_rows, + columns=orders_cols, + autoHeight=True, + density="compact", + disableColumnSelector=True, + hideFooter=True, + initialState={"columns": {"columnVisibilityModel": {"id": False}}}) + else: + mui.Typography(str(orders), sx={"fontSize": "0.75rem"}) + + # Trades Table + mui.Typography("Recent Trades", variant="h6", sx={"marginTop": 2}) + df_trades = pd.DataFrame(bot_config["trades"]) + + # Add 'id' column to the dataframe by concatenating 'trade_id' and 'trade_timestamp' + df_trades['id'] = df_trades.get('trade_id', '0').astype(str) + df_trades['trade_timestamp'].astype(str) + + # Convert timestamp col to datetime + df_trades['trade_timestamp'] = df_trades['trade_timestamp'].astype(int) + + # Show last X trades + df_trades = df_trades.sort_values(by='trade_timestamp', ascending=False) + df_trades = df_trades.head(TRADES_TO_SHOW) + df_trades['time_ago'] = df_trades['trade_timestamp'].apply(time_ago) + + trades_rows = df_trades.to_dict(orient='records') + trades_cols = [{'field': col, 'headerName': col} for col in df_trades.columns] + + for column in trades_cols: + # Customize width for 'market' column + if column['field'] == 'market': + column['width'] = WIDE_COL_WIDTH + if column['field'] == 'trade_timestamp': + column['width'] = MEDIUM_COL_WIDTH + + mui.DataGrid(rows=trades_rows, + columns=trades_cols, + autoHeight=True, + density="compact", + disableColumnSelector=True, + hideFooter=True, + initialState={"columns": {"columnVisibilityModel": {"id": False, "trade_id": False, "trade_timestamp": False, "base_asset": False, "quote_asset": False, "raw_json": False}}}) else: with mui.CardContent(sx={"flex": 1}): with mui.Grid(container=True, spacing=2): diff --git a/utils/status_parser.py b/utils/status_parser.py new file mode 100644 index 00000000..3558b908 --- /dev/null +++ b/utils/status_parser.py @@ -0,0 +1,109 @@ +class StatusParser: + def __init__(self, input_str, type='orders'): + self.lines = input_str.split("\n") + + if type == 'orders': + if "No active maker orders" in input_str: + self.parser = self + elif all(keyword in input_str for keyword in ['Orders:','Exchange', 'Market', 'Side', 'Price', 'Amount', 'Age']): + self.parser = OrdersParser(self.lines, ['Exchange', 'Market', 'Side', 'Price', 'Amount', 'Age']) + elif all(keyword in input_str for keyword in ['Orders:','Level', 'Amount (Orig)', 'Amount (Adj)']): + self.parser = OrdersParser(self.lines, ['Level', 'Type', 'Price', 'Spread', 'Amount (Orig)', 'Amount (Adj)', 'Age']) + else: + raise ValueError("No matching string for type 'order'") + elif type == 'balances': + self.parser = BalancesParser(self.lines) + # if all(keyword in input_str for keyword in ['Balances:']): + else: + raise ValueError(f"Unsupported type: {type}") + + def parse(self): + return self.parser._parse() + + def _parse(self): + if "No active maker orders" in self.lines: + return "No active maker orders" + raise NotImplementedError + +class OrdersParser: + def __init__(self, lines, columns): + self.lines = lines + self.columns = columns + + def _parse(self): + if "No active maker orders" in "\n".join(self.lines): + return "No active maker orders" + + orders = [] + for i, line in enumerate(self.lines): + if "Orders:" in line: + start_idx = i + 1 + break + + lines = self.lines[start_idx + 1:] + for line in lines: + + # Ignore warning lines + # if line.startswith("***"): + # break + + # Break when there's a blank line + if not line.strip(): + break + + parts = line.split() + if len(parts) < len(self.columns): + continue + + # Create the orders dictionary based on provided columns + order = {} + for idx, col in enumerate(self.columns): + order[col] = parts[idx] + + # Special handling for 'id' column (concatenating several parts) + if 'id' not in order: + order['id'] = ''.join(parts[:len(self.columns)-1]) + + orders.append(order) + + return orders + +class BalancesParser: + def __init__(self, lines): + self.lines = lines + self.columns = ['Exchange', 'Asset', 'Total Balance', 'Available Balance'] + + def _parse(self): + # Check if "Balances:" exists in the lines + if not any("Balances:" in line for line in self.lines): + return "No balances" + + balances = [] + for i, line in enumerate(self.lines): + if "Balances:" in line: + start_idx = i + 1 + break + + lines = self.lines[start_idx + 1:] + for line in lines: + + # Break when there's a blank line + if not line.strip(): + break + + parts = line.split() + if len(parts) < len(self.columns): + continue + + # Create the balances dictionary based on provided columns + balance = {} + for idx, col in enumerate(self.columns): + balance[col] = parts[idx] + + # Special handling for 'id' column (concatenating several parts) + if 'id' not in balance: + balance['id'] = ''.join(parts[:len(self.columns)-1]) + + balances.append(balance) + + return balances \ No newline at end of file