diff --git a/backend/services/backend_api_client.py b/backend/services/backend_api_client.py index 8be28af..9bc128b 100644 --- a/backend/services/backend_api_client.py +++ b/backend/services/backend_api_client.py @@ -1,5 +1,8 @@ +from typing import Optional, Dict + import pandas as pd import requests +import streamlit as st from hummingbot.strategy_v2.models.executors_info import ExecutorInfo @@ -12,7 +15,7 @@ class BackendAPIClient: _shared_instance = None @classmethod - def get_instance(cls, *args, **kwargs) -> "MarketsRecorder": + def get_instance(cls, *args, **kwargs) -> "BackendAPIClient": if cls._shared_instance is None: cls._shared_instance = BackendAPIClient(*args, **kwargs) return cls._shared_instance @@ -22,158 +25,164 @@ class BackendAPIClient: self.port = port self.base_url = f"http://{self.host}:{self.port}" + def post(self, endpoint: str, payload: Optional[Dict] = None, params: Optional[Dict] = None): + """ + Post request to the backend API. + :param params: + :param endpoint: + :param payload: + :return: + """ + url = f"{self.base_url}/{endpoint}" + response = requests.post(url, json=payload, params=params) + return self._process_response(response) + + def get(self, endpoint: str): + """ + Get request to the backend API. + :param endpoint: + :return: + """ + url = f"{self.base_url}/{endpoint}" + response = requests.get(url) + return self._process_response(response) + + @staticmethod + def _process_response(response): + if response.status_code == 400: + st.error(response.json()["detail"]) + return + return response.json() + def is_docker_running(self): """Check if Docker is running.""" - url = f"{self.base_url}/is-docker-running" - response = requests.get(url) - return response.json() + endpoint = "is-docker-running" + return self.get(endpoint)["is_docker_running"] def pull_image(self, image_name: str): """Pull a Docker image.""" - url = f"{self.base_url}/pull-image/" - payload = {"image_name": image_name} - response = requests.post(url, json=payload) - return response.json() + endpoint = "pull-image" + return self.post(endpoint, payload={"image_name": image_name}) def list_available_images(self, image_name: str): """List available images by name.""" - url = f"{self.base_url}/available-images/{image_name}" - response = requests.get(url) - return response.json() + endpoint = f"available-images/{image_name}" + return self.get(endpoint) def list_active_containers(self): """List all active containers.""" - url = f"{self.base_url}/active-containers" - response = requests.get(url) - return response.json() + endpoint = "active-containers" + return self.get(endpoint) def list_exited_containers(self): """List all exited containers.""" - url = f"{self.base_url}/exited-containers" - response = requests.get(url) - return response.json() + endpoint = "exited-containers" + return self.get(endpoint) def clean_exited_containers(self): """Clean up exited containers.""" - url = f"{self.base_url}/clean-exited-containers" - response = requests.post(url) - return response.json() + endpoint = "clean-exited-containers" + return self.post(endpoint, payload=None) def remove_container(self, container_name: str, archive_locally: bool = True, s3_bucket: str = None): """Remove a specific container.""" - url = f"{self.base_url}/remove-container/{container_name}" + endpoint = f"remove-container/{container_name}" params = {"archive_locally": archive_locally} if s3_bucket: params["s3_bucket"] = s3_bucket - response = requests.post(url, params=params) - return response.json() + return self.post(endpoint, params=params) def stop_container(self, container_name: str): """Stop a specific container.""" - url = f"{self.base_url}/stop-container/{container_name}" - response = requests.post(url) - return response.json() + endpoint = f"stop-container/{container_name}" + return self.post(endpoint) def start_container(self, container_name: str): """Start a specific container.""" - url = f"{self.base_url}/start-container/{container_name}" - response = requests.post(url) - return response.json() + endpoint = f"start-container/{container_name}" + return self.post(endpoint) def create_hummingbot_instance(self, instance_config: dict): """Create a new Hummingbot instance.""" - url = f"{self.base_url}/create-hummingbot-instance" - response = requests.post(url, json=instance_config) - return response.json() + endpoint = "create-hummingbot-instance" + return self.post(endpoint, payload=instance_config) def start_bot(self, start_bot_config: dict): """Start a Hummingbot bot.""" - url = f"{self.base_url}/start-bot" - response = requests.post(url, json=start_bot_config) - return response.json() + endpoint = "start-bot" + return self.post(endpoint, payload=start_bot_config) def stop_bot(self, bot_name: str, skip_order_cancellation: bool = False, async_backend: bool = True): """Stop a Hummingbot bot.""" - url = f"{self.base_url}/stop-bot" - response = requests.post(url, json={"bot_name": bot_name, "skip_order_cancellation": skip_order_cancellation, "async_backend": async_backend}) - return response.json() + endpoint = "stop-bot" + return self.post(endpoint, payload={"bot_name": bot_name, "skip_order_cancellation": skip_order_cancellation, "async_backend": async_backend}) def import_strategy(self, strategy_config: dict): """Import a trading strategy to a bot.""" - url = f"{self.base_url}/import-strategy" - response = requests.post(url, json=strategy_config) - return response.json() + endpoint = "import-strategy" + return self.post(endpoint, payload=strategy_config) def get_bot_status(self, bot_name: str): """Get the status of a bot.""" - url = f"{self.base_url}/get-bot-status/{bot_name}" - response = requests.get(url) - if response.status_code == 200: - return response.json() - else: - return {"status": "error", "data": "Bot not found"} + endpoint = f"get-bot-status/{bot_name}" + return self.get(endpoint) def get_bot_history(self, bot_name: str): """Get the historical data of a bot.""" - url = f"{self.base_url}/get-bot-history/{bot_name}" - response = requests.get(url) - return response.json() + endpoint = f"get-bot-history/{bot_name}" + return self.get(endpoint) def get_active_bots_status(self): """ Retrieve the cached status of all active bots. Returns a JSON response with the status and data of active bots. """ - url = f"{self.base_url}/get-active-bots-status" - response = requests.get(url) - if response.status_code == 200: - return response.json() # Successful request - else: - return {"status": "error", "data": "No active bots found"} + endpoint = "get-active-bots-status" + return self.get(endpoint) def get_all_controllers_config(self): """Get all controller configurations.""" - url = f"{self.base_url}/all-controller-configs" - response = requests.get(url) - return response.json() + endpoint = "all-controller-configs" + return self.get(endpoint) def get_available_images(self, image_name: str = "hummingbot"): """Get available images.""" - url = f"{self.base_url}/available-images/{image_name}" - response = requests.get(url) - return response.json()["available_images"] + endpoint = f"available-images/{image_name}" + return self.get(endpoint)["available_images"] def add_script_config(self, script_config: dict): """Add a new script configuration.""" - url = f"{self.base_url}/add-script-config" - response = requests.post(url, json=script_config) - return response.json() + endpoint = "add-script-config" + return self.post(endpoint, payload=script_config) def add_controller_config(self, controller_config: dict): """Add a new controller configuration.""" - url = f"{self.base_url}/add-controller-config" + endpoint = "add-controller-config" config = { "name": controller_config["id"], "content": controller_config } - response = requests.post(url, json=config) - return response.json() + return self.post(endpoint, payload=config) + + def delete_controller_config(self, controller_name: str): + """Delete a controller configuration.""" + url = "delete-controller-config" + return self.post(url, params={"config_name": controller_name}) def get_real_time_candles(self, connector: str, trading_pair: str, interval: str, max_records: int): """Get candles data.""" - url = f"{self.base_url}/real-time-candles" + endpoint = "real-time-candles" payload = { "connector": connector, "trading_pair": trading_pair, "interval": interval, "max_records": max_records } - response = requests.post(url, json=payload) - return response.json() + return self.post(endpoint, payload=payload) def get_historical_candles(self, connector: str, trading_pair: str, interval: str, start_time: int, end_time: int): """Get historical candles data.""" - url = f"{self.base_url}/historical-candles" + endpoint = "historical-candles" payload = { "connector": connector, "trading_pair": trading_pair, @@ -181,12 +190,11 @@ class BackendAPIClient: "start_time": start_time, "end_time": end_time } - response = requests.post(url, json=payload) - return response.json() + return self.post(endpoint, payload=payload) def run_backtesting(self, start_time: int, end_time: int, backtesting_resolution: str, trade_cost: float, config: dict): """Run backtesting.""" - url = f"{self.base_url}/run-backtesting" + endpoint = "run-backtesting" payload = { "start_time": start_time, "end_time": end_time, @@ -194,90 +202,86 @@ class BackendAPIClient: "trade_cost": trade_cost, "config": config } - response = requests.post(url, json=payload) - backtesting_results = response.json() + backtesting_results = self.post(endpoint, payload=payload) if "error" in backtesting_results: raise Exception(backtesting_results["error"]) if "processed_data" not in backtesting_results: data = None else: data = pd.DataFrame(backtesting_results["processed_data"]) + if "executors" not in backtesting_results: + executors = [] + else: + executors = [ExecutorInfo(**executor) for executor in backtesting_results["executors"]] return { "processed_data": data, - "executors": [ExecutorInfo(**executor) for executor in backtesting_results["executors"]], + "executors": executors, "results": backtesting_results["results"] } def get_all_configs_from_bot(self, bot_name: str): """Get all configurations from a bot.""" - url = f"{self.base_url}/all-controller-configs/bot/{bot_name}" - response = requests.get(url) - return response.json() + endpoint = f"all-controller-configs/bot/{bot_name}" + return self.get(endpoint) def stop_controller_from_bot(self, bot_name: str, controller_id: str): """Stop a controller from a bot.""" + endpoint = f"update-controller-config/bot/{bot_name}/{controller_id}" config = {"manual_kill_switch": True} - url = f"{self.base_url}/update-controller-config/bot/{bot_name}/{controller_id}" - response = requests.post(url, json=config) - return response.json() + return self.post(endpoint, payload=config) def start_controller_from_bot(self, bot_name: str, controller_id: str): """Start a controller from a bot.""" + endpoint = f"update-controller-config/bot/{bot_name}/{controller_id}" config = {"manual_kill_switch": False} - url = f"{self.base_url}/update-controller-config/bot/{bot_name}/{controller_id}" - response = requests.post(url, json=config) - return response.json() + return self.post(endpoint, payload=config) def get_connector_config_map(self, connector_name: str): """Get connector configuration map.""" - url = f"{self.base_url}/connector-config-map/{connector_name}" - response = requests.get(url) - return response.json() + endpoint = f"connector-config-map/{connector_name}" + return self.get(endpoint) def get_all_connectors_config_map(self): """Get all connector configuration maps.""" - url = f"{self.base_url}/all-connectors-config-map" - response = requests.get(url) - return response.json() + endpoint = "all-connectors-config-map" + return self.get(endpoint) def add_account(self, account_name: str): """Add a new account.""" - url = f"{self.base_url}/add-account" - response = requests.post(url, params={"account_name": account_name}) - return response.json() + endpoint = "add-account" + return self.post(endpoint, params={"account_name": account_name}) def delete_account(self, account_name: str): """Delete an account.""" - url = f"{self.base_url}/delete-account/" - response = requests.post(url, params={"account_name": account_name}) - return response.json() + endpoint = "delete-account" + return self.post(endpoint, params={"account_name": account_name}) def delete_credential(self, account_name: str, connector_name: str): """Delete credentials.""" - url = f"{self.base_url}/delete-credential/{account_name}/{connector_name}" - response = requests.post(url) - return response.json() + endpoint = f"delete-credential/{account_name}/{connector_name}" + return self.post(endpoint) def add_connector_keys(self, account_name: str, connector_name: str, connector_config: dict): """Add connector keys.""" - url = f"{self.base_url}/add-connector-keys/{account_name}/{connector_name}" - response = requests.post(url, json=connector_config) - return response.json() + endpoint = f"add-connector-keys/{account_name}/{connector_name}" + return self.post(endpoint, payload=connector_config) def get_accounts(self): """Get available credentials.""" - url = f"{self.base_url}/list-accounts" - response = requests.get(url) - return response.json() + endpoint = "list-accounts" + return self.get(endpoint) def get_credentials(self, account_name: str): """Get available credentials.""" - url = f"{self.base_url}/list-credentials/{account_name}" - response = requests.get(url) - return response.json() + endpoint = f"list-credentials/{account_name}" + return self.get(endpoint) - def get_all_balances(self): + def get_accounts_state(self): """Get all balances.""" - url = f"{self.base_url}/get-all-balances" - response = requests.get(url) - return response.json() + endpoint = "accounts-state" + return self.get(endpoint) + + def get_account_state_history(self): + """Get account state history.""" + endpoint = "account-state-history" + return self.get(endpoint) diff --git a/environment_conda.yml b/environment_conda.yml index c903dd2..41f9baf 100644 --- a/environment_conda.yml +++ b/environment_conda.yml @@ -8,6 +8,7 @@ dependencies: - pip - pip: - hummingbot + - numpy==1.26.4 - streamlit==1.33.0 - watchdog - python-dotenv diff --git a/frontend/components/bot_performance_card.py b/frontend/components/bot_performance_card.py index c045bc9..e567ae3 100644 --- a/frontend/components/bot_performance_card.py +++ b/frontend/components/bot_performance_card.py @@ -1,17 +1,18 @@ -import time - +import pandas as pd +import streamlit as st +from hummingbot.strategy_v2.models.executors import CloseType from streamlit_elements import mui -from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT from frontend.components.dashboard import Dashboard -from backend.services.backend_api_client import BackendAPIClient +from frontend.st_utils import get_backend_api_client TRADES_TO_SHOW = 5 -WIDE_COL_WIDTH = 250 -MEDIUM_COL_WIDTH = 170 -SMALL_COL_WIDTH = 100 -backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +ULTRA_WIDE_COL_WIDTH = 300 +WIDE_COL_WIDTH = 160 +MEDIUM_COL_WIDTH = 140 +SMALL_COL_WIDTH = 110 +backend_api_client = get_backend_api_client() def stop_bot(bot_name): @@ -26,19 +27,23 @@ def archive_bot(bot_name): class BotPerformanceCardV2(Dashboard.Item): DEFAULT_COLUMNS = [ {"field": 'id', "headerName": 'ID', "width": WIDE_COL_WIDTH}, + {"field": 'controller', "headerName": 'Controller', "width": SMALL_COL_WIDTH, "editable": False}, + {"field": 'connector', "headerName": 'Connector', "width": SMALL_COL_WIDTH, "editable": False}, + {"field": 'trading_pair', "headerName": 'Trading Pair', "width": SMALL_COL_WIDTH, "editable": False}, {"field": 'realized_pnl_quote', "headerName": 'Realized PNL ($)', "width": MEDIUM_COL_WIDTH, "editable": False}, {"field": 'unrealized_pnl_quote', "headerName": 'Unrealized PNL ($)', "width": MEDIUM_COL_WIDTH, "editable": False}, {"field": 'global_pnl_quote', "headerName": 'NET PNL ($)', "width": MEDIUM_COL_WIDTH, "editable": False}, - {"field": 'volume_traded', "headerName": 'Volume ($)', "width": MEDIUM_COL_WIDTH, "editable": False}, - {"field": 'open_order_volume', "headerName": 'Open Order Volume ($)', "width": MEDIUM_COL_WIDTH, "editable": False}, - {"field": 'imbalance', "headerName": 'Imbalance ($)', "width": MEDIUM_COL_WIDTH, "editable": False}, + {"field": 'volume_traded', "headerName": 'Volume ($)', "width": SMALL_COL_WIDTH, "editable": False}, + {"field": 'open_order_volume', "headerName": 'Liquidity Placed ($)', "width": MEDIUM_COL_WIDTH, "editable": False}, + {"field": 'imbalance', "headerName": 'Imbalance ($)', "width": SMALL_COL_WIDTH, "editable": False}, + {"field": 'close_types', "headerName": 'Close Types', "width": ULTRA_WIDE_COL_WIDTH, "editable": False} ] _active_controller_config_selected = [] _stopped_controller_config_selected = [] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) + self._backend_api_client = get_backend_api_client() def _handle_stopped_row_selection(self, params, _): self._stopped_controller_config_selected = params @@ -88,6 +93,8 @@ class BotPerformanceCardV2(Dashboard.Item): bot_data = bot_status.get("data") is_running = bot_data.get("status") == "running" performance = bot_data.get("performance") + error_logs = bot_data.get("error_logs") + general_logs = bot_data.get("general_logs") if is_running: for controller, inner_dict in performance.items(): controller_status = inner_dict.get("status") @@ -97,6 +104,9 @@ class BotPerformanceCardV2(Dashboard.Item): continue controller_performance = inner_dict.get("performance") controller_config = next((config for config in controller_configs if config.get("id") == controller), {}) + controller_name = controller_config.get("controller_name", controller) + connector_name = controller_config.get("connector_name", "NaN") + trading_pair = controller_config.get("trading_pair", "NaN") kill_switch_status = True if controller_config.get("manual_kill_switch") is True else False realized_pnl_quote = controller_performance.get("realized_pnl_quote", 0) unrealized_pnl_quote = controller_performance.get("unrealized_pnl_quote", 0) @@ -104,14 +114,25 @@ class BotPerformanceCardV2(Dashboard.Item): volume_traded = controller_performance.get("volume_traded", 0) open_order_volume = controller_performance.get("open_order_volume", 0) imbalance = controller_performance.get("imbalance", 0) + close_types = controller_performance.get("close_type_counts", {}) + tp = close_types.get("CloseType.TAKE_PROFIT", 0) + sl = close_types.get("CloseType.STOP_LOSS", 0) + time_limit = close_types.get("CloseType.TIME_LIMIT", 0) + ts = close_types.get("CloseType.TRAILING_STOP", 0) + refreshed = close_types.get("CloseType.EARLY_STOP", 0) + close_types_str = f"TP: {tp} | SL: {sl} | TS: {ts} | TL: {time_limit} | RS: {refreshed}" controller_info = { "id": controller, - "realized_pnl_quote": realized_pnl_quote, - "unrealized_pnl_quote": unrealized_pnl_quote, - "global_pnl_quote": global_pnl_quote, - "volume_traded": volume_traded, - "open_order_volume": open_order_volume, - "imbalance": imbalance, + "controller": controller_name, + "connector": connector_name, + "trading_pair": trading_pair, + "realized_pnl_quote": round(realized_pnl_quote, 2), + "unrealized_pnl_quote": round(unrealized_pnl_quote, 2), + "global_pnl_quote": round(global_pnl_quote, 2), + "volume_traded": round(volume_traded, 2), + "open_order_volume": round(open_order_volume, 2), + "imbalance": round(imbalance, 2), + "close_types": close_types_str, } if kill_switch_status: stopped_controllers_list.append(controller_info) @@ -269,6 +290,30 @@ class BotPerformanceCardV2(Dashboard.Item): sx={"width": "100%", "height": "100%"}): mui.icon.AddCircleOutline() mui.Typography("Stop") + with mui.Accordion(sx={"padding": "10px 15px 10px 15px"}): + with mui.AccordionSummary(expandIcon=mui.icon.ExpandMoreIcon()): + mui.Typography("Error Logs") + with mui.AccordionDetails(sx={"display": "flex", "flexDirection": "column"}): + if len(error_logs) > 0: + for log in error_logs[:50]: + timestamp = log.get("timestamp") + message = log.get("msg") + logger_name = log.get("logger_name") + mui.Typography(f"{timestamp} - {logger_name}: {message}") + else: + mui.Typography("No error logs available.") + with mui.Accordion(sx={"padding": "10px 15px 10px 15px"}): + with mui.AccordionSummary(expandIcon=mui.icon.ExpandMoreIcon()): + mui.Typography("General Logs") + with mui.AccordionDetails(sx={"display": "flex", "flexDirection": "column"}): + if len(general_logs) > 0: + for log in general_logs[:50]: + timestamp = pd.to_datetime(int(log.get("timestamp")), unit="s") + message = log.get("msg") + logger_name = log.get("logger_name") + mui.Typography(f"{timestamp} - {logger_name}: {message}") + else: + mui.Typography("No general logs available.") except Exception as e: print(e) with mui.Card(key=self._key, diff --git a/frontend/components/config_loader.py b/frontend/components/config_loader.py index 3f5ff1f..03a74d9 100644 --- a/frontend/components/config_loader.py +++ b/frontend/components/config_loader.py @@ -1,17 +1,31 @@ import streamlit as st -from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT -from backend.services.backend_api_client import BackendAPIClient +from frontend.st_utils import get_backend_api_client +from frontend.utils import generate_random_name -backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +backend_api_client = get_backend_api_client() def get_default_config_loader(controller_name: str): - use_default_config = st.checkbox("Use default config", value=True) all_configs = backend_api_client.get_all_controllers_config() - if use_default_config: - st.session_state["default_config"] = {} - else: - configs = [config for config in all_configs if config["controller_name"] == controller_name] - default_config = st.selectbox("Select a config", [config["id"] for config in configs]) - st.session_state["default_config"] = next((config for config in all_configs if config["id"] == default_config), {}) + existing_configs = [config["id"].split("_")[0] for config in all_configs] + default_dict = {"id": generate_random_name(existing_configs)} + default_config = st.session_state.get("default_config", default_dict) + config_controller_name = default_config.get("controller_name") + if default_config is None or controller_name != config_controller_name: + st.session_state["default_config"] = default_dict + with st.expander("Configurations", expanded=True): + c1, c2 = st.columns(2) + with c1: + use_default_config = st.checkbox("Use default config", value=True) + with c2: + if not use_default_config: + configs = [config for config in all_configs if config["controller_name"] == controller_name] + if len(configs) > 0: + default_config = st.selectbox("Select a config", [config["id"] for config in configs]) + st.session_state["default_config"] = next((config for config in all_configs if config["id"] == default_config), None) + st.session_state["default_config"]["id"] = st.session_state["default_config"]["id"].split("_")[0] + else: + st.warning("No existing configs found for this controller.") + + diff --git a/frontend/components/deploy_v2_with_controllers.py b/frontend/components/deploy_v2_with_controllers.py index 9faae93..ef39165 100644 --- a/frontend/components/deploy_v2_with_controllers.py +++ b/frontend/components/deploy_v2_with_controllers.py @@ -4,6 +4,7 @@ import pandas as pd from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT from backend.services.backend_api_client import BackendAPIClient +from frontend.st_utils import get_backend_api_client class LaunchV2WithControllers: @@ -14,11 +15,11 @@ class LaunchV2WithControllers: ] def __init__(self): - self._backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) + self._backend_api_client = get_backend_api_client() self._controller_configs_available = self._backend_api_client.get_all_controllers_config() self._controller_config_selected = [] self._bot_name = None - self._image_name = "dardonacci/hummingbot:latest" + self._image_name = "hummingbot/hummingbot:latest" self._credentials = "master_account" def _set_bot_name(self, bot_name): diff --git a/frontend/components/executors_distribution.py b/frontend/components/executors_distribution.py index 0b77495..5cdae99 100644 --- a/frontend/components/executors_distribution.py +++ b/frontend/components/executors_distribution.py @@ -2,10 +2,16 @@ import streamlit as st from frontend.components.st_inputs import get_distribution, normalize, distribution_inputs -def get_executors_distribution_inputs(default_spreads=[0.01, 0.02], default_amounts=[0.2, 0.8]): +def get_executors_distribution_inputs(use_custom_spread_units=False): + default_amounts = [0.2, 0.8] default_config = st.session_state.get("default_config", {}) - buy_spreads = default_config.get("buy_spreads", default_spreads) - sell_spreads = default_config.get("sell_spreads", default_spreads) + if use_custom_spread_units: + buy_spreads = [spread / 100 for spread in default_config.get("buy_spreads", [1, 2])] + sell_spreads = [spread / 100 for spread in default_config.get("sell_spreads", [1, 2])] + else: + buy_spreads = default_config.get("buy_spreads", [0.01, 0.02]) + sell_spreads = default_config.get("sell_spreads", [0.01, 0.02]) + buy_amounts_pct = default_config.get("buy_amounts_pct", default_amounts) sell_amounts_pct = default_config.get("sell_amounts_pct", default_amounts) buy_order_levels_def = len(buy_spreads) diff --git a/frontend/components/launch_strategy_v2.py b/frontend/components/launch_strategy_v2.py index 810957a..268faf4 100644 --- a/frontend/components/launch_strategy_v2.py +++ b/frontend/components/launch_strategy_v2.py @@ -6,12 +6,14 @@ from streamlit_elements import mui, lazy from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT from backend.services.backend_api_client import BackendAPIClient from .dashboard import Dashboard +from ..st_utils import get_backend_api_client class LaunchStrategyV2(Dashboard.Item): DEFAULT_ROWS = [] DEFAULT_COLUMNS = [ - {"field": 'id', "headerName": 'ID', "width": 230}, + {"field": 'config_base', "headerName": 'Config Base', "minWidth": 160, "editable": False, }, + {"field": 'version', "headerName": 'Version', "minWidth": 100, "editable": False, }, {"field": 'controller_name', "headerName": 'Controller Name', "width": 150, "editable": False, }, {"field": 'controller_type', "headerName": 'Controller Type', "width": 150, "editable": False, }, {"field": 'connector_name', "headerName": 'Connector', "width": 150, "editable": False, }, @@ -26,11 +28,11 @@ class LaunchStrategyV2(Dashboard.Item): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) + self._backend_api_client = get_backend_api_client() self._controller_configs_available = self._backend_api_client.get_all_controllers_config() self._controller_config_selected = None self._bot_name = None - self._image_name = "dardonacci/hummingbot:latest" + self._image_name = "hummingbot/hummingbot:latest" self._credentials = "master_account" def _set_bot_name(self, event): @@ -79,18 +81,34 @@ class LaunchStrategyV2(Dashboard.Item): st.warning("You need to define the bot name and select the controllers configs " "that you want to deploy.") + def delete_selected_configs(self): + if self._controller_config_selected: + for config in self._controller_config_selected: + response = self._backend_api_client.delete_controller_config(config) + st.success(response) + self._controller_configs_available = self._backend_api_client.get_all_controllers_config() + else: + st.warning("You need to select the controllers configs that you want to delete.") + def __call__(self): with mui.Paper(key=self._key, sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, elevation=1): with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False): - mui.Typography("🚀 Select the controller configs to launch", variant="h5") + mui.Typography("🎛️ Bot Configuration", variant="h5") with mui.Grid(container=True, spacing=2, sx={"padding": "10px 15px 10px 15px"}): - with mui.Grid(item=True, xs=8): - mui.Alert( - "The new instance will contain the credentials configured in the following base instance:", - severity="info") + with mui.Grid(item=True, xs=4): + mui.TextField(label="Instance Name", variant="outlined", onChange=lazy(self._set_bot_name), + sx={"width": "100%"}) + with mui.Grid(item=True, xs=4): + available_images = self._backend_api_client.get_available_images("hummingbot") + with mui.FormControl(variant="standard", sx={"width": "100%"}): + mui.FormHelperText("Available Images") + with mui.Select(label="Hummingbot Image", defaultValue="hummingbot/hummingbot:latest", + variant="standard", onChange=lazy(self._set_image_name)): + for image in available_images: + mui.MenuItem(image, value=image) with mui.Grid(item=True, xs=4): available_credentials = self._backend_api_client.get_accounts() with mui.FormControl(variant="standard", sx={"width": "100%"}): @@ -99,24 +117,6 @@ class LaunchStrategyV2(Dashboard.Item): variant="standard", onChange=lazy(self._set_credentials)): for master_config in available_credentials: mui.MenuItem(master_config, value=master_config) - with mui.Grid(item=True, xs=4): - mui.TextField(label="Instance Name", variant="outlined", onChange=lazy(self._set_bot_name), - sx={"width": "100%"}) - with mui.Grid(item=True, xs=4): - available_images = self._backend_api_client.get_available_images("hummingbot") - with mui.FormControl(variant="standard", sx={"width": "100%"}): - mui.FormHelperText("Available Images") - with mui.Select(label="Hummingbot Image", defaultValue="dardonacci/hummingbot:latest", - variant="standard", onChange=lazy(self._set_image_name)): - for image in available_images: - mui.MenuItem(image, value=image) - with mui.Grid(item=True, xs=4): - with mui.Button(onClick=self.launch_new_bot, - variant="outlined", - color="success", - sx={"width": "100%", "height": "100%"}): - mui.icon.AddCircleOutline() - mui.Typography("Create") all_controllers_config = self._backend_api_client.get_all_controllers_config() data = [] for config in all_controllers_config: @@ -127,28 +127,47 @@ class LaunchStrategyV2(Dashboard.Item): take_profit = config.get("take_profit", 0) trailing_stop = config.get("trailing_stop", {"activation_price": 0, "trailing_delta": 0}) time_limit = config.get("time_limit", 0) - data.append({"id": config["id"], "controller_name": config["controller_name"], - "controller_type": config["controller_type"], - "connector_name": connector_name, - "trading_pair": trading_pair, - "total_amount_quote": total_amount_quote, - "max_loss_quote": total_amount_quote * stop_loss / 2, - "stop_loss": stop_loss, - "take_profit": take_profit, - "trailing_stop": str(trailing_stop["activation_price"]) + " / " + - str(trailing_stop["trailing_delta"]), - "time_limit": time_limit}) + config_version = config["id"].split("_") + if len(config_version) > 1: + config_base = config_version[0] + version = config_version[1] + else: + config_base = config["id"] + version = "NaN" + data.append({ + "id": config["id"], "config_base": config_base, "version": version, + "controller_name": config["controller_name"], "controller_type": config["controller_type"], + "connector_name": connector_name, "trading_pair": trading_pair, + "total_amount_quote": total_amount_quote, "max_loss_quote": total_amount_quote * stop_loss / 2, + "stop_loss": stop_loss, "take_profit": take_profit, + "trailing_stop": str(trailing_stop["activation_price"]) + " / " + + str(trailing_stop["trailing_delta"]), + "time_limit": time_limit}) with mui.Grid(item=True, xs=12): - mui.Alert("Select the controller configs to deploy", severity="info") with mui.Paper(key=self._key, sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden", "height": 1000}, - elevation=1): + elevation=2): with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False): - mui.icon.ViewCompact() - mui.Typography("Controllers Config") - with mui.Box(sx={"flex": 1, "minHeight": 3}): + with mui.Grid(container=True, spacing=2): + with mui.Grid(item=True, xs=8): + mui.Typography("🗄️ Available Configurations", variant="h6") + with mui.Grid(item=True, xs=2): + with mui.Button(onClick=self.delete_selected_configs, + variant="outlined", + color="error", + sx={"width": "100%", "height": "100%"}): + mui.icon.Delete() + mui.Typography("Delete") + with mui.Grid(item=True, xs=2): + with mui.Button(onClick=self.launch_new_bot, + variant="outlined", + color="success", + sx={"width": "100%", "height": "100%"}): + mui.icon.AddCircleOutline() + mui.Typography("Launch Bot") + with mui.Box(sx={"flex": 1, "minHeight": 3, "width": "100%"}): mui.DataGrid( columns=self.DEFAULT_COLUMNS, rows=data, @@ -156,5 +175,6 @@ class LaunchStrategyV2(Dashboard.Item): rowsPerPageOptions=[15], checkboxSelection=True, disableSelectionOnClick=True, + disableColumnResize=False, onSelectionModelChange=self._handle_row_selection, ) diff --git a/frontend/components/save_config.py b/frontend/components/save_config.py index 89896d7..a23dc80 100644 --- a/frontend/components/save_config.py +++ b/frontend/components/save_config.py @@ -2,21 +2,30 @@ import streamlit as st from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT from backend.services.backend_api_client import BackendAPIClient +from frontend.st_utils import get_backend_api_client -def render_save_config(controller_name: str, config_data: dict): +def render_save_config(config_base_default: str, config_data: dict): st.write("### Upload Config to BackendAPI") + backend_api_client = get_backend_api_client() + all_configs = backend_api_client.get_all_controllers_config() + config_bases = set(config_name["id"].split("_")[0] for config_name in all_configs) + config_base = config_base_default.split("_")[0] + if config_base in config_bases: + config_tag = max(float(config["id"].split("_")[-1]) for config in all_configs if config_base in config["id"]) + version, tag = str(config_tag).split(".") + config_tag = f"{version}.{int(tag) + 1}" + else: + config_tag = "0.1" c1, c2, c3 = st.columns([1, 1, 0.5]) - connector = config_data.get("connector_name", "") - trading_pair = config_data.get("trading_pair", "") with c1: - config_base = st.text_input("Config Base", value=f"{controller_name}-{connector}-{trading_pair.split('-')[0]}") + config_base = st.text_input("Config Base", value=config_base) with c2: - config_tag = st.text_input("Config Tag", value="1.1") + config_tag = st.text_input("Config Tag", value=config_tag) with c3: upload_config_to_backend = st.button("Upload") if upload_config_to_backend: - config_data["id"] = f"{config_base}-{config_tag}" - backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) + config_data["id"] = f"{config_base}_{config_tag}" backend_api_client.add_controller_config(config_data) + st.session_state.pop("default_config") st.success("Config uploaded successfully!") diff --git a/frontend/pages/config/bollinger_v1/app.py b/frontend/pages/config/bollinger_v1/app.py index 9f6ec6d..b0bbdd0 100644 --- a/frontend/pages/config/bollinger_v1/app.py +++ b/frontend/pages/config/bollinger_v1/app.py @@ -1,17 +1,11 @@ -from datetime import datetime - import streamlit as st -import pandas as pd -import yaml import pandas_ta as ta # noqa: F401 -from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT -from backend.services.backend_api_client import BackendAPIClient from frontend.components.backtesting import backtesting_section from frontend.components.config_loader import get_default_config_loader from frontend.components.save_config import render_save_config -from frontend.pages.config.utils import get_max_records, get_candles -from frontend.st_utils import initialize_st_page +from frontend.pages.config.utils import get_candles +from frontend.st_utils import initialize_st_page, get_backend_api_client from frontend.pages.config.bollinger_v1.user_inputs import user_inputs from plotly.subplots import make_subplots @@ -26,17 +20,17 @@ from frontend.visualization.utils import add_traces_to_fig # Initialize the Streamlit page initialize_st_page(title="Bollinger V1", icon="📈", initial_sidebar_state="expanded") -backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +backend_api_client = get_backend_api_client() st.text("This tool will let you create a config for Bollinger V1 and visualize the strategy.") get_default_config_loader("bollinger_v1") inputs = user_inputs() -st.session_state["default_config"] = inputs +st.session_state["default_config"].update(inputs) st.write("### Visualizing Bollinger Bands and Trading Signals") -days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=3) +days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=7) # Load candle data candles = get_candles(connector_name=inputs["candles_connector"], trading_pair=inputs["candles_trading_pair"], interval=inputs["interval"], days=days_to_visualize) @@ -68,4 +62,4 @@ if bt_results: st.write("---") render_close_types(bt_results["results"]) st.write("---") -render_save_config("bollinger_v1", inputs) +render_save_config(st.session_state["default_config"]["id"], st.session_state["default_config"]) diff --git a/frontend/pages/config/dman_maker_v2/app.py b/frontend/pages/config/dman_maker_v2/app.py index 7fbd2b0..830fe99 100644 --- a/frontend/pages/config/dman_maker_v2/app.py +++ b/frontend/pages/config/dman_maker_v2/app.py @@ -7,7 +7,7 @@ from frontend.components.config_loader import get_default_config_loader from frontend.components.dca_distribution import get_dca_distribution_inputs from frontend.components.save_config import render_save_config from frontend.pages.config.dman_maker_v2.user_inputs import user_inputs -from frontend.st_utils import initialize_st_page +from frontend.st_utils import initialize_st_page, get_backend_api_client from frontend.visualization.backtesting import create_backtesting_figure from frontend.visualization.backtesting_metrics import render_backtesting_metrics, render_accuracy_metrics, \ render_close_types @@ -16,7 +16,7 @@ from frontend.visualization.executors_distribution import create_executors_distr # Initialize the Streamlit page initialize_st_page(title="D-Man Maker V2", icon="🧙‍♂️") -backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +backend_api_client = get_backend_api_client() # Page content @@ -52,7 +52,7 @@ st.plotly_chart(fig, use_container_width=True) # Combine inputs and dca_inputs into final config config = {**inputs, **dca_inputs} -st.session_state["default_config"] = config +st.session_state["default_config"].update(config) bt_results = backtesting_section(config, backend_api_client) if bt_results: fig = create_backtesting_figure( @@ -68,4 +68,4 @@ if bt_results: st.write("---") render_close_types(bt_results["results"]) st.write("---") -render_save_config("dman_maker_v2", config) \ No newline at end of file +render_save_config(st.session_state["default_config"]["id"], st.session_state["default_config"]) diff --git a/frontend/pages/config/dman_v5/app.py b/frontend/pages/config/dman_v5/app.py index ac39d9f..9522cb8 100644 --- a/frontend/pages/config/dman_v5/app.py +++ b/frontend/pages/config/dman_v5/app.py @@ -6,7 +6,7 @@ from plotly.subplots import make_subplots from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT from backend.services.backend_api_client import BackendAPIClient -from frontend.st_utils import initialize_st_page +from frontend.st_utils import initialize_st_page, get_backend_api_client # Initialize the Streamlit page initialize_st_page(title="D-Man V5", icon="📊", initial_sidebar_state="expanded") @@ -142,6 +142,6 @@ with c3: if upload_config_to_backend: - backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) + backend_api_client = get_backend_api_client() backend_api_client.add_controller_config(config) st.success("Config uploaded successfully!") diff --git a/frontend/pages/config/kalman_filter_v1/app.py b/frontend/pages/config/kalman_filter_v1/app.py index 5f7e358..2479d18 100644 --- a/frontend/pages/config/kalman_filter_v1/app.py +++ b/frontend/pages/config/kalman_filter_v1/app.py @@ -7,7 +7,7 @@ from pykalman import KalmanFilter from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT from backend.services.backend_api_client import BackendAPIClient -from frontend.st_utils import initialize_st_page +from frontend.st_utils import initialize_st_page, get_backend_api_client # Initialize the Streamlit page initialize_st_page(title="Kalman Filter V1", icon="📈", initial_sidebar_state="expanded") @@ -220,6 +220,6 @@ with c3: if upload_config_to_backend: - backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) + backend_api_client = get_backend_api_client() backend_api_client.add_controller_config(config) st.success("Config uploaded successfully!") \ No newline at end of file diff --git a/frontend/pages/config/macd_bb_v1/app.py b/frontend/pages/config/macd_bb_v1/app.py index f061fcc..3857587 100644 --- a/frontend/pages/config/macd_bb_v1/app.py +++ b/frontend/pages/config/macd_bb_v1/app.py @@ -1,37 +1,33 @@ import streamlit as st -import pandas as pd -import plotly.graph_objects as go -import yaml from plotly.subplots import make_subplots -from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT -from backend.services.backend_api_client import BackendAPIClient from frontend.components.backtesting import backtesting_section from frontend.components.config_loader import get_default_config_loader from frontend.components.save_config import render_save_config from frontend.pages.config.macd_bb_v1.user_inputs import user_inputs -from frontend.pages.config.utils import get_candles, get_max_records -from frontend.st_utils import initialize_st_page +from frontend.pages.config.utils import get_candles +from frontend.st_utils import initialize_st_page, get_backend_api_client from frontend.visualization import theme from frontend.visualization.backtesting import create_backtesting_figure from frontend.visualization.backtesting_metrics import render_backtesting_metrics, render_accuracy_metrics, \ render_close_types from frontend.visualization.candles import get_candlestick_trace -from frontend.visualization.indicators import get_bbands_traces, get_volume_trace, get_macd_traces +from frontend.visualization.indicators import get_bbands_traces, get_macd_traces from frontend.visualization.signals import get_macdbb_v1_signal_traces from frontend.visualization.utils import add_traces_to_fig # Initialize the Streamlit page initialize_st_page(title="MACD_BB V1", icon="📊", initial_sidebar_state="expanded") -backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +backend_api_client = get_backend_api_client() get_default_config_loader("macd_bb_v1") # User inputs inputs = user_inputs() -st.session_state["default_config"] = inputs +st.session_state["default_config"].update(inputs) + st.write("### Visualizing MACD Bollinger Trading Signals") -days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=3) +days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=7) # Load candle data candles = get_candles(connector_name=inputs["candles_connector"], trading_pair=inputs["candles_trading_pair"], interval=inputs["interval"], days=days_to_visualize) @@ -64,4 +60,5 @@ if bt_results: st.write("---") render_close_types(bt_results["results"]) st.write("---") -render_save_config("bollinger_v1", inputs) +render_save_config(st.session_state["default_config"]["id"], st.session_state["default_config"]) + diff --git a/frontend/pages/config/pmm_dynamic/app.py b/frontend/pages/config/pmm_dynamic/app.py index 418512d..f154444 100644 --- a/frontend/pages/config/pmm_dynamic/app.py +++ b/frontend/pages/config/pmm_dynamic/app.py @@ -2,8 +2,6 @@ import streamlit as st import plotly.graph_objects as go from plotly.subplots import make_subplots -from backend.services.backend_api_client import BackendAPIClient -from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT from frontend.components.config_loader import get_default_config_loader from frontend.components.executors_distribution import get_executors_distribution_inputs from frontend.components.save_config import render_save_config @@ -12,8 +10,8 @@ from frontend.components.save_config import render_save_config from frontend.components.backtesting import backtesting_section from frontend.pages.config.pmm_dynamic.spread_and_price_multipliers import get_pmm_dynamic_multipliers from frontend.pages.config.pmm_dynamic.user_inputs import user_inputs -from frontend.pages.config.utils import get_max_records, get_candles -from frontend.st_utils import initialize_st_page +from frontend.pages.config.utils import get_candles +from frontend.st_utils import initialize_st_page, get_backend_api_client from frontend.visualization import theme from frontend.visualization.backtesting import create_backtesting_figure from frontend.visualization.candles import get_candlestick_trace @@ -25,7 +23,7 @@ from frontend.visualization.utils import add_traces_to_fig # Initialize the Streamlit page initialize_st_page(title="PMM Dynamic", icon="👩‍🏫") -backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +backend_api_client = get_backend_api_client() # Page content st.text("This tool will let you create a config for PMM Dynamic, backtest and upload it to the Backend API.") @@ -35,7 +33,7 @@ inputs = user_inputs() st.write("### Visualizing MACD and NATR indicators for PMM Dynamic") st.text("The MACD is used to shift the mid price and the NATR to make the spreads dynamic. " "In the order distributions graph, we are going to see the values of the orders affected by the average NATR") -days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=3) +days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=7) # Load candle data candles = get_candles(connector_name=inputs["candles_connector"], trading_pair=inputs["candles_trading_pair"], interval=inputs["interval"], days=days_to_visualize) with st.expander("Visualizing PMM Dynamic Indicators", expanded=True): @@ -55,12 +53,12 @@ with st.expander("Visualizing PMM Dynamic Indicators", expanded=True): st.write("### Executors Distribution") st.write("The order distributions are affected by the average NATR. This means that if the first order has a spread of " "1 and the NATR is 0.005, the first order will have a spread of 0.5% of the mid price.") -buy_spread_distributions, sell_spread_distributions, buy_order_amounts_pct, sell_order_amounts_pct = get_executors_distribution_inputs() +buy_spread_distributions, sell_spread_distributions, buy_order_amounts_pct, sell_order_amounts_pct = get_executors_distribution_inputs(use_custom_spread_units=True) inputs["buy_spreads"] = [spread * 100 for spread in buy_spread_distributions] inputs["sell_spreads"] = [spread * 100 for spread in sell_spread_distributions] inputs["buy_amounts_pct"] = buy_order_amounts_pct inputs["sell_amounts_pct"] = sell_order_amounts_pct -st.session_state["default_config"] = inputs +st.session_state["default_config"].update(inputs) with st.expander("Executor Distribution:", expanded=True): natr_avarage = spreads_multiplier.mean() buy_spreads = [spread * natr_avarage for spread in inputs["buy_spreads"]] @@ -84,4 +82,4 @@ if bt_results: st.write("---") render_close_types(bt_results["results"]) st.write("---") -render_save_config("pmm_dynamic", inputs) +render_save_config(st.session_state["default_config"]["id"], st.session_state["default_config"]) diff --git a/frontend/pages/config/pmm_simple/app.py b/frontend/pages/config/pmm_simple/app.py index 28b9c75..445c362 100644 --- a/frontend/pages/config/pmm_simple/app.py +++ b/frontend/pages/config/pmm_simple/app.py @@ -7,7 +7,7 @@ from frontend.components.save_config import render_save_config # Import submodules from frontend.pages.config.pmm_simple.user_inputs import user_inputs from frontend.components.backtesting import backtesting_section -from frontend.st_utils import initialize_st_page +from frontend.st_utils import initialize_st_page, get_backend_api_client from frontend.visualization.backtesting import create_backtesting_figure from frontend.visualization.executors_distribution import create_executors_distribution_traces from frontend.visualization.backtesting_metrics import render_backtesting_metrics, render_close_types, \ @@ -15,13 +15,16 @@ from frontend.visualization.backtesting_metrics import render_backtesting_metric # Initialize the Streamlit page initialize_st_page(title="PMM Simple", icon="👨‍🏫") -backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +backend_api_client = get_backend_api_client() + # Page content st.text("This tool will let you create a config for PMM Simple, backtest and upload it to the Backend API.") get_default_config_loader("pmm_simple") + inputs = user_inputs() +st.session_state["default_config"].update(inputs) with st.expander("Executor Distribution:", expanded=True): fig = create_executors_distribution_traces(inputs["buy_spreads"], inputs["sell_spreads"], inputs["buy_amounts_pct"], inputs["sell_amounts_pct"], inputs["total_amount_quote"]) st.plotly_chart(fig, use_container_width=True) @@ -41,4 +44,4 @@ if bt_results: st.write("---") render_close_types(bt_results["results"]) st.write("---") -render_save_config("pmm_simple", inputs) +render_save_config(st.session_state["default_config"]["id"], st.session_state["default_config"]) diff --git a/frontend/pages/config/pmm_simple/user_inputs.py b/frontend/pages/config/pmm_simple/user_inputs.py index 69ff319..b160481 100644 --- a/frontend/pages/config/pmm_simple/user_inputs.py +++ b/frontend/pages/config/pmm_simple/user_inputs.py @@ -35,5 +35,4 @@ def user_inputs(): "trailing_delta": ts_delta } } - st.session_state["default_config"] = config return config diff --git a/frontend/pages/config/supertrend_v1/app.py b/frontend/pages/config/supertrend_v1/app.py index af195d3..83e6633 100644 --- a/frontend/pages/config/supertrend_v1/app.py +++ b/frontend/pages/config/supertrend_v1/app.py @@ -1,14 +1,12 @@ import streamlit as st from plotly.subplots import make_subplots -from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT -from backend.services.backend_api_client import BackendAPIClient from frontend.components.backtesting import backtesting_section from frontend.components.config_loader import get_default_config_loader from frontend.components.save_config import render_save_config from frontend.pages.config.supertrend_v1.user_inputs import user_inputs -from frontend.pages.config.utils import get_candles, get_max_records -from frontend.st_utils import initialize_st_page +from frontend.pages.config.utils import get_candles +from frontend.st_utils import initialize_st_page, get_backend_api_client from frontend.visualization import theme from frontend.visualization.backtesting import create_backtesting_figure from frontend.visualization.backtesting_metrics import render_backtesting_metrics, render_accuracy_metrics, \ @@ -20,15 +18,15 @@ from frontend.visualization.utils import add_traces_to_fig # Initialize the Streamlit page initialize_st_page(title="SuperTrend V1", icon="📊", initial_sidebar_state="expanded") -backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +backend_api_client = get_backend_api_client() get_default_config_loader("supertrend_v1") # User inputs inputs = user_inputs() -st.session_state["default_config"] = inputs +st.session_state["default_config"].update(inputs) st.write("### Visualizing Supertrend Trading Signals") -days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=3) +days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=7) # Load candle data candles = get_candles(connector_name=inputs["candles_connector"], trading_pair=inputs["candles_trading_pair"], interval=inputs["interval"], days=days_to_visualize) @@ -61,4 +59,4 @@ if bt_results: st.write("---") render_close_types(bt_results["results"]) st.write("---") -render_save_config("bollinger_v1", inputs) +render_save_config(st.session_state["default_config"]["id"], st.session_state["default_config"]) diff --git a/frontend/pages/config/xemm_controller/app.py b/frontend/pages/config/xemm_controller/app.py index 00bd029..f8431e8 100644 --- a/frontend/pages/config/xemm_controller/app.py +++ b/frontend/pages/config/xemm_controller/app.py @@ -4,7 +4,7 @@ import yaml from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT from backend.services.backend_api_client import BackendAPIClient -from frontend.st_utils import initialize_st_page +from frontend.st_utils import initialize_st_page, get_backend_api_client # Initialize the Streamlit page initialize_st_page(title="XEMM Multiple Levels", icon="⚡️") @@ -104,11 +104,11 @@ st.plotly_chart(sell_order_fig, use_container_width=True) # Display in Streamlit c1, c2, c3 = st.columns([2, 2, 1]) with c1: - config_base = st.text_input("Config Base", value=f"xemm_{maker_connector}_{taker_connector}-{maker_trading_pair.split('-')[0]}") + config_base = st.text_input("Config Base", value=f"xemm-{maker_connector}-{taker_connector}-{maker_trading_pair.split('-')[0]}") with c2: config_tag = st.text_input("Config Tag", value="1.1") -id = f"{config_base}-{config_tag}" +id = f"{config_base}_{config_tag}" config = { "id": id.lower(), "controller_name": "xemm_multiple_levels", @@ -125,16 +125,10 @@ config = { yaml_config = yaml.dump(config, default_flow_style=False) with c3: - download_config = st.download_button( - label="Download YAML", - data=yaml_config, - file_name=f'{id.lower()}.yml', - mime='text/yaml' - ) upload_config_to_backend = st.button("Upload Config to BackendAPI") if upload_config_to_backend: - backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) + backend_api_client = get_backend_api_client() backend_api_client.add_controller_config(config) st.success("Config uploaded successfully!") \ No newline at end of file diff --git a/frontend/pages/data/download_candles/app.py b/frontend/pages/data/download_candles/app.py index ee1a7cc..d03aca0 100644 --- a/frontend/pages/data/download_candles/app.py +++ b/frontend/pages/data/download_candles/app.py @@ -3,12 +3,11 @@ from datetime import datetime, time import pandas as pd import plotly.graph_objects as go -from backend.services.backend_api_client import BackendAPIClient -from frontend.st_utils import initialize_st_page +from frontend.st_utils import initialize_st_page, get_backend_api_client # Initialize Streamlit page initialize_st_page(title="Download Candles", icon="💾") -backend_api_client = BackendAPIClient.get_instance() +backend_api_client = get_backend_api_client() c1, c2, c3, c4 = st.columns([2, 2, 2, 0.5]) with c1: diff --git a/frontend/pages/orchestration/credentials/app.py b/frontend/pages/orchestration/credentials/app.py index c719fe3..666f054 100644 --- a/frontend/pages/orchestration/credentials/app.py +++ b/frontend/pages/orchestration/credentials/app.py @@ -1,13 +1,13 @@ from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT from backend.services.backend_api_client import BackendAPIClient -from frontend.st_utils import initialize_st_page +from frontend.st_utils import initialize_st_page, get_backend_api_client import streamlit as st initialize_st_page(title="Credentials", icon="🔑") # Page content -client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +client = get_backend_api_client() NUM_COLUMNS = 4 @@ -36,13 +36,20 @@ else: st.markdown("---") -c1, c2 = st.columns([1, 1]) +c1, c2, c3 = st.columns([1, 1, 1]) with c1: # Section to create a new account st.header("Create a New Account") new_account_name = st.text_input("New Account Name") if st.button("Create Account"): + new_account_name = new_account_name.replace(" ", "_") if new_account_name: + if new_account_name in accounts: + st.warning(f"Account {new_account_name} already exists.") + st.stop() + elif new_account_name == "" or all(char == "_" for char in new_account_name): + st.warning("Please enter a valid account name.") + st.stop() response = client.add_account(new_account_name) st.write(response) else: @@ -55,7 +62,20 @@ with c2: if st.button("Delete Account"): if delete_account_name and delete_account_name != "No accounts available": response = client.delete_account(delete_account_name) - st.write(response) + st.warning(response) + else: + st.write("Please select a valid account.") + +with c3: + # Section to delete a credential from an existing account + st.header("Delete Credential") + delete_account_cred_name = st.selectbox("Select the credentials account", options=accounts if accounts else ["No accounts available"],) + creds_for_account = [credential.split(".")[0] for credential in client.get_credentials(delete_account_cred_name)] + delete_cred_name = st.selectbox("Select a Credential to Delete", options=creds_for_account if creds_for_account else ["No credentials available"]) + if st.button("Delete Credential"): + if (delete_account_cred_name and delete_account_cred_name != "No accounts available") and (delete_cred_name and delete_cred_name != "No credentials available"): + response = client.delete_credential(delete_account_cred_name, delete_cred_name) + st.warning(response) else: st.write("Please select a valid account.") @@ -70,15 +90,17 @@ with c2: all_connectors = list(all_connector_config_map.keys()) binance_perpetual_index = all_connectors.index("binance_perpetual") if "binance_perpetual" in all_connectors else None connector_name = st.selectbox("Select Connector", options=all_connectors, index=binance_perpetual_index) -if account_name and account_name != "No accounts available" and connector_name: - st.write(f"Configuration Map for {connector_name}:") config_map = all_connector_config_map[connector_name] - config_inputs = {} - cols = st.columns(NUM_COLUMNS) - for i, config in enumerate(config_map): - with cols[i % (NUM_COLUMNS - 1)]: - config_inputs[config] = st.text_input(config) - with cols[NUM_COLUMNS - 1]: - if st.button("Submit Credentials"): - response = client.add_connector_keys(account_name, connector_name, config_inputs) - st.write(response) + +st.write(f"Configuration Map for {connector_name}:") +config_inputs = {} +cols = st.columns(NUM_COLUMNS) +for i, config in enumerate(config_map): + with cols[i % (NUM_COLUMNS - 1)]: + config_inputs[config] = st.text_input(config, type="password", key=f"{connector_name}_{config}") + +with cols[-1]: + if st.button("Submit Credentials"): + response = client.add_connector_keys(account_name, connector_name, config_inputs) + if response: + st.success(response) \ No newline at end of file diff --git a/frontend/pages/orchestration/instances/app.py b/frontend/pages/orchestration/instances/app.py index 512c65c..e854da4 100644 --- a/frontend/pages/orchestration/instances/app.py +++ b/frontend/pages/orchestration/instances/app.py @@ -8,7 +8,7 @@ from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT from frontend.components.bot_performance_card import BotPerformanceCardV2 from frontend.components.dashboard import Dashboard from backend.services.backend_api_client import BackendAPIClient -from frontend.st_utils import initialize_st_page +from frontend.st_utils import initialize_st_page, get_backend_api_client # Constants for UI layout CARD_WIDTH = 12 @@ -38,7 +38,7 @@ def update_active_bots(api_client): initialize_st_page(title="Instances", icon="🦅") -api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +api_client = get_backend_api_client() if not api_client.is_docker_running(): st.warning("Docker is not running. Please start Docker and refresh the page.") @@ -71,5 +71,5 @@ with elements("active_instances_board"): card(bot) while True: - time.sleep(5) + time.sleep(10) st.rerun() \ No newline at end of file diff --git a/frontend/pages/orchestration/portfolio/app.py b/frontend/pages/orchestration/portfolio/app.py index 286bc1b..27d8607 100644 --- a/frontend/pages/orchestration/portfolio/app.py +++ b/frontend/pages/orchestration/portfolio/app.py @@ -1,58 +1,154 @@ -from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT -from backend.services.backend_api_client import BackendAPIClient -from frontend.st_utils import initialize_st_page +from frontend.st_utils import initialize_st_page, get_backend_api_client import streamlit as st import pandas as pd +import plotly.graph_objects as go +import plotly.express as px initialize_st_page(title="Portfolio", icon="💰") # Page content -client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) +client = get_backend_api_client() NUM_COLUMNS = 4 -@st.cache_data -def get_all_balances(): - return client.get_all_balances() - -# Fetch all balances -balances = get_all_balances() # Convert balances to a DataFrame for easier manipulation -def balances_to_df(balances): +def account_state_to_df(account_state): data = [] - for account, exchanges in balances.items(): - for exchange, tokens in exchanges.items(): - for token, amount in tokens.items(): - data.append({"Account": account, "Exchange": exchange, "Token": token, "Amount": amount}) + for account, exchanges in account_state.items(): + for exchange, tokens_info in exchanges.items(): + for info in tokens_info: + data.append({ + "account": account, + "exchange": exchange, + "token": info["token"], + "price": info["price"], + "units": info["units"], + "value": info["value"], + "available_units": info["available_units"], + }) return pd.DataFrame(data) -df_balances = balances_to_df(balances) -c1, c2 = st.columns([1, 1]) -with c1: - st.header("Current Balances") -with c2: - st.header("Aggregated Balances") -c1, c2, c3, c4 = st.columns([2.5, 1.5, 1.5, 1.1]) -with c1: - # Display balances - st.subheader("All Balances") - st.dataframe(df_balances) +# Convert historical account states to a DataFrame +def account_history_to_df(history): + data = [] + for record in history: + timestamp = record["timestamp"] + for account, exchanges in record["state"].items(): + for exchange, tokens_info in exchanges.items(): + for info in tokens_info: + data.append({ + "timestamp": timestamp, + "account": account, + "exchange": exchange, + "token": info["token"], + "price": info["price"], + "units": info["units"], + "value": info["value"], + "available_units": info["available_units"], + }) + return pd.DataFrame(data) -with c2: - # Aggregation at the account level - account_agg = df_balances.groupby(["Account", "Token"])["Amount"].sum().reset_index() - st.subheader("Account Level") - st.dataframe(account_agg) -with c3: - # Aggregation at the exchange level - exchange_agg = df_balances.groupby(["Exchange", "Token"])["Amount"].sum().reset_index() - st.subheader("Exchange Level") - st.dataframe(exchange_agg) +# Fetch account state from the backend +account_state = client.get_accounts_state() +account_history = client.get_account_state_history() +if len(account_state) == 0: + st.warning("No accounts found.") + st.stop() -with c4: - # Overall holdings - overall_agg = df_balances.groupby("Token")["Amount"].sum().reset_index() - st.subheader("Token Level") - st.write(overall_agg) +# Display the accounts available +accounts = st.multiselect("Select Accounts", list(account_state.keys()), list(account_state.keys())) +if len(accounts) == 0: + st.warning("Please select an account.") + st.stop() + +# Display the exchanges available +exchanges_available = [] +for account in accounts: + exchanges_available += account_state[account].keys() + +if len(exchanges_available) == 0: + st.warning("No exchanges found.") + st.stop() +exchanges = st.multiselect("Select Exchanges", exchanges_available, exchanges_available) + +# Display the tokens available +tokens_available = [] +for account in accounts: + for exchange in exchanges: + if exchange in account_state[account]: + tokens_available += [info["token"] for info in account_state[account][exchange]] + +token_options = set(tokens_available) +tokens_available = st.multiselect("Select Tokens", token_options, token_options) + + +st.write("---") + +filtered_account_state = {} +for account in accounts: + filtered_account_state[account] = {} + for exchange in exchanges: + if exchange in account_state[account]: + filtered_account_state[account][exchange] = [token_info for token_info in account_state[account][exchange] if token_info["token"] in tokens_available] + +filtered_account_history = [] +for record in account_history: + filtered_record = {"timestamp": record["timestamp"], "state": {}} + for account in accounts: + if account in record["state"]: + filtered_record["state"][account] = {} + for exchange in exchanges: + if exchange in record["state"][account]: + filtered_record["state"][account][exchange] = [token_info for token_info in record["state"][account][exchange] if token_info["token"] in tokens_available] + filtered_account_history.append(filtered_record) + +if len(filtered_account_state) > 0: + account_state_df = account_state_to_df(filtered_account_state) + total_balance_usd = round(account_state_df["value"].sum(), 2) + c1, c2 = st.columns([1, 5]) + with c1: + st.metric("Total Balance (USD)", total_balance_usd) + with c2: + account_state_df['% Allocation'] = (account_state_df['value'] / total_balance_usd) * 100 + account_state_df['label'] = account_state_df['token'] + ' ($' + account_state_df['value'].apply( + lambda x: f'{x:,.2f}') + ')' + + # Create a sunburst chart with Plotly Express + fig = px.sunburst(account_state_df, + path=['account', 'exchange', 'label'], + values='value', + hover_data={'% Allocation': ':.2f'}, + title='% Allocation by Account, Exchange, and Token', + color='account', + color_discrete_sequence=px.colors.qualitative.Vivid) + + fig.update_traces(textinfo='label+percent entry') + + fig.update_layout(margin=dict(t=0, l=0, r=0, b=0), height=800, title_x=0.01, title_y=1,) + + st.plotly_chart(fig, use_container_width=True) + + st.dataframe(account_state_df[['exchange', 'token', 'units', 'price', 'value', 'available_units']], width=1800, + height=600) + +# Plot the evolution of the portfolio over time +if len(filtered_account_history) > 0: + account_history_df = account_history_to_df(filtered_account_history) + account_history_df['timestamp'] = pd.to_datetime(account_history_df['timestamp']) + + # Aggregate the value of the portfolio over time + portfolio_evolution_df = account_history_df.groupby('timestamp')['value'].sum().reset_index() + + fig = px.line(portfolio_evolution_df, x='timestamp', y='value', title='Portfolio Evolution Over Time') + fig.update_layout(xaxis_title='Time', yaxis_title='Total Value (USD)', height=600) + st.plotly_chart(fig, use_container_width=True) + + # Plot the evolution of each token's value over time + token_evolution_df = account_history_df.groupby(['timestamp', 'token'])['value'].sum().reset_index() + + fig = px.area(token_evolution_df, x='timestamp', y='value', color='token', title='Token Value Evolution Over Time', + color_discrete_sequence=px.colors.qualitative.Vivid) + fig.update_layout(xaxis_title='Time', yaxis_title='Value (USD)', height=600) + st.plotly_chart(fig, use_container_width=True) diff --git a/frontend/st_utils.py b/frontend/st_utils.py index 82575ca..6336e33 100644 --- a/frontend/st_utils.py +++ b/frontend/st_utils.py @@ -66,3 +66,18 @@ def style_metric_cards( unsafe_allow_html=True, ) + +def get_backend_api_client(): + from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT + from backend.services.backend_api_client import BackendAPIClient + backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT) + is_docker_running = False + try: + is_docker_running = backend_api_client.is_docker_running() + except Exception as e: + st.error(f"There was an error trying to connect to the Backend API: \n\n{str(e)} \n\nPlease make sure the Backend API is running.") + st.stop() + if not is_docker_running: + st.error("Docker is not running. Please make sure Docker is running.") + st.stop() + return backend_api_client diff --git a/frontend/utils.py b/frontend/utils.py new file mode 100644 index 0000000..4059d11 --- /dev/null +++ b/frontend/utils.py @@ -0,0 +1,33 @@ +import random + + +def generate_random_name(existing_names_list): + # Convert the list to a set for efficient lookup + existing_names = set(existing_names_list) + + money_related = ["dollar", "coin", "credit", "wealth", "fortune", "cash", "gold", "profit", "rich", "value"] + trading_related = ["market", "trade", "exchange", "broker", "stock", "bond", "option", "margin", "future", "index"] + algorithm_related = ["algo", "bot", "code", "script", "logic", "matrix", "compute", "sequence", "data", "binary"] + science_related = ["quantum", "neuron", "atom", "fusion", "gravity", "particle", "genome", "spectrum", "theory", + "experiment"] + space_related = ["galaxy", "nebula", "star", "planet", "orbit", "cosmos", "asteroid", "comet", "blackhole", + "eclipse"] + bird_related = ["falcon", "eagle", "hawk", "sparrow", "robin", "swallow", "owl", "raven", "dove", "phoenix"] + + categories = [money_related, trading_related, algorithm_related, science_related, space_related, bird_related] + + while True: + # Select two different categories + first_category = random.choice(categories) + second_category = random.choice([category for category in categories if category != first_category]) + + # Select one word from each category + first_word = random.choice(first_category) + second_word = random.choice(second_category) + + name = f"{first_word}-{second_word}" + + if name not in existing_names: + existing_names.add(name) + existing_names_list.append(name) # Update the list to keep track of used names + return name diff --git a/main.py b/main.py index 218827f..d037b1a 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ def main_page(): Page("main.py", "Hummingbot Dashboard", "📊"), Section("Bot Orchestration", "🐙"), Page("frontend/pages/orchestration/instances/app.py", "Instances", "🦅"), - Page("frontend/pages/orchestration/launch_bot_v2/app.py", "Deploy", "🚀"), + Page("frontend/pages/orchestration/launch_bot_v2/app.py", "Deploy V2", "🚀"), Page("frontend/pages/orchestration/credentials/app.py", "Credentials", "🔑"), Page("frontend/pages/orchestration/portfolio/app.py", "Portfolio", "💰"), # Page("frontend/pages/orchestration/launch_bot_v2_st/app.py", "Deploy ST", "🙌"),