Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/improve status #63

Merged
merged 12 commits into from
Aug 29, 2023
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,6 @@ quants_lab/optimizations/*
quants_lab/strategy/experiments/*

# Master bot template user-added configs
hummingbot_files/templates/master_bot_conf/conf/*
hummingbot_files/templates/master_bot_conf/conf/*

**/.DS_Store
8 changes: 4 additions & 4 deletions pages/bot_orchestration/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
114 changes: 102 additions & 12 deletions ui_components/bot_performance_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down Expand Up @@ -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"},
Expand All @@ -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):
Expand Down
109 changes: 109 additions & 0 deletions utils/status_parser.py
Original file line number Diff line number Diff line change
@@ -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