diff --git a/backend/__init__.py b/backend/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/backend/services/__init__.py b/backend/services/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/backend/services/backend_api_client.py b/backend/services/backend_api_client.py
deleted file mode 100644
index 9f38e1e9..00000000
--- a/backend/services/backend_api_client.py
+++ /dev/null
@@ -1,365 +0,0 @@
-from typing import Any, Dict, List, Optional
-
-import pandas as pd
-import requests
-import streamlit as st
-from hummingbot.strategy_v2.models.executors_info import ExecutorInfo
-from requests.auth import HTTPBasicAuth
-
-
-class BackendAPIClient:
- """
- This class is a client to interact with the backend API. The Backend API is a REST API that provides endpoints to
- create new Hummingbot instances, start and stop them, add new script and controller config files, and get the status
- of the active bots.
- """
- _shared_instance = None
-
- @classmethod
- def get_instance(cls, *args, **kwargs) -> "BackendAPIClient":
- if cls._shared_instance is None:
- cls._shared_instance = BackendAPIClient(*args, **kwargs)
- return cls._shared_instance
-
- def __init__(self, host: str = "localhost", port: int = 8000, username: str = "admin", password: str = "admin"):
- self.host = host
- self.port = port
- self.base_url = f"http://{self.host}:{self.port}"
- self.auth = HTTPBasicAuth(username, password)
-
- 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, auth=self.auth)
- return self._process_response(response)
-
- def get(self, endpoint: str) -> Any:
- """
- Get request to the backend API.
- :param endpoint:
- :return:
- """
- url = f"{self.base_url}/{endpoint}"
- response = requests.get(url, auth=self.auth)
- return self._process_response(response)
-
- @staticmethod
- def _process_response(response):
- if response.status_code == 401:
- st.error("You are not authorized to access Backend API. Please check your credentials.")
- return
- elif response.status_code == 400:
- st.error(response.json()["detail"])
- return
- return response.json()
-
- def is_docker_running(self):
- """Check if Docker is running."""
- endpoint = "is-docker-running"
- return self.get(endpoint)["is_docker_running"]
-
- def pull_image(self, image_name: str):
- """Pull a Docker image."""
- 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."""
- endpoint = f"available-images/{image_name}"
- return self.get(endpoint)
-
- def list_active_containers(self):
- """List all active containers."""
- endpoint = "active-containers"
- return self.get(endpoint)
-
- def list_exited_containers(self):
- """List all exited containers."""
- endpoint = "exited-containers"
- return self.get(endpoint)
-
- def clean_exited_containers(self):
- """Clean up exited containers."""
- 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."""
- endpoint = f"remove-container/{container_name}"
- params = {"archive_locally": archive_locally}
- if s3_bucket:
- params["s3_bucket"] = s3_bucket
- return self.post(endpoint, params=params)
-
- def stop_container(self, container_name: str):
- """Stop a specific container."""
- endpoint = f"stop-container/{container_name}"
- return self.post(endpoint)
-
- def start_container(self, container_name: str):
- """Start a specific container."""
- endpoint = f"start-container/{container_name}"
- return self.post(endpoint)
-
- def create_hummingbot_instance(self, instance_config: dict):
- """Create a new Hummingbot instance."""
- endpoint = "create-hummingbot-instance"
- return self.post(endpoint, payload=instance_config)
-
- def start_bot(self, start_bot_config: dict):
- """Start a Hummingbot bot."""
- 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."""
- 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."""
- endpoint = "import-strategy"
- return self.post(endpoint, payload=strategy_config)
-
- def get_bot_status(self, bot_name: str):
- """Get the status of a bot."""
- 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."""
- 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.
- """
- endpoint = "get-active-bots-status"
- return self.get(endpoint)
-
- def get_all_controllers_config(self) -> List[dict]:
- """Get all controller configurations."""
- endpoint = "all-controller-configs"
- return self.get(endpoint)
-
- def get_available_images(self, image_name: str = "hummingbot"):
- """Get 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."""
- endpoint = "add-script-config"
- return self.post(endpoint, payload=script_config)
-
- def add_controller_config(self, controller_config: dict):
- """Add a new controller configuration."""
- endpoint = "add-controller-config"
- config = {
- "name": controller_config["id"],
- "content": controller_config
- }
- 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 delete_script_config(self, script_name: str):
- """Delete a script configuration."""
- url = "delete-script-config"
- return self.post(url, params={"script_name": script_name})
-
- def delete_all_controller_configs(self):
- """Delete all controller configurations."""
- endpoint = "delete-all-controller-configs"
- return self.post(endpoint)
-
- def delete_all_script_configs(self):
- """Delete all script configurations."""
- endpoint = "delete-all-script-configs"
- return self.post(endpoint)
-
- def get_real_time_candles(self, connector: str, trading_pair: str, interval: str, max_records: int):
- """Get candles data."""
- endpoint = "real-time-candles"
- payload = {
- "connector": connector,
- "trading_pair": trading_pair,
- "interval": interval,
- "max_records": max_records
- }
- 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."""
- endpoint = "historical-candles"
- payload = {
- "connector_name": connector,
- "trading_pair": trading_pair,
- "interval": interval,
- "start_time": start_time,
- "end_time": end_time
- }
- 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."""
- endpoint = "run-backtesting"
- payload = {
- "start_time": start_time,
- "end_time": end_time,
- "backtesting_resolution": backtesting_resolution,
- "trade_cost": trade_cost,
- "config": config
- }
- 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": executors,
- "results": backtesting_results["results"]
- }
-
- def get_all_configs_from_bot(self, bot_name: str):
- """Get all configurations from a bot."""
- 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}
- 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}
- return self.post(endpoint, payload=config)
-
- def get_connector_config_map(self, connector_name: str):
- """Get connector configuration map."""
- endpoint = f"connector-config-map/{connector_name}"
- return self.get(endpoint)
-
- def get_all_connectors_config_map(self):
- """Get all connector configuration maps."""
- endpoint = "all-connectors-config-map"
- return self.get(endpoint)
-
- def add_account(self, account_name: str):
- """Add a new account."""
- endpoint = "add-account"
- return self.post(endpoint, params={"account_name": account_name})
-
- def delete_account(self, account_name: str):
- """Delete an account."""
- endpoint = "delete-account"
- return self.post(endpoint, params={"account_name": account_name})
-
- def delete_credential(self, account_name: str, connector_name: str):
- """Delete credentials."""
- 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."""
- endpoint = f"add-connector-keys/{account_name}/{connector_name}"
- return self.post(endpoint, payload=connector_config)
-
- def get_accounts(self):
- """Get available credentials."""
- endpoint = "list-accounts"
- return self.get(endpoint)
-
- def get_credentials(self, account_name: str):
- """Get available credentials."""
- endpoint = f"list-credentials/{account_name}"
- return self.get(endpoint)
-
- def get_accounts_state(self):
- """Get all balances."""
- 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)
-
- def get_performance_results(self, executors: List[Dict[str, Any]]):
- if not isinstance(executors, list) or len(executors) == 0:
- raise ValueError("Executors must be a non-empty list of dictionaries")
- # Check if all elements in executors are dictionaries
- if not all(isinstance(executor, dict) for executor in executors):
- raise ValueError("All elements in executors must be dictionaries")
- endpoint = "get-performance-results"
- payload = {
- "executors": executors,
- }
-
- performance_results = self.post(endpoint, payload=payload)
- if "error" in performance_results:
- raise Exception(performance_results["error"])
- if "detail" in performance_results:
- raise Exception(performance_results["detail"])
- if "processed_data" not in performance_results:
- data = None
- else:
- data = pd.DataFrame(performance_results["processed_data"])
- if "executors" not in performance_results:
- executors = []
- else:
- executors = [ExecutorInfo(**executor) for executor in performance_results["executors"]]
- return {
- "processed_data": data,
- "executors": executors,
- "results": performance_results["results"]
- }
-
- def list_databases(self):
- """Get databases list."""
- endpoint = "list-databases"
- return self.post(endpoint)
-
- def read_databases(self, db_paths: List[str]):
- """Read databases."""
- endpoint = "read-databases"
- return self.post(endpoint, payload=db_paths)
-
- def create_checkpoint(self, db_names: List[str]):
- """Create a checkpoint."""
- endpoint = "create-checkpoint"
- return self.post(endpoint, payload=db_names)
-
- def list_checkpoints(self, full_path: bool):
- """List checkpoints."""
- endpoint = "list-checkpoints"
- params = {"full_path": full_path}
- return self.post(endpoint, params=params)
-
- def load_checkpoint(self, checkpoint_path: str):
- """Load a checkpoint."""
- endpoint = "load-checkpoint"
- params = {"checkpoint_path": checkpoint_path}
- return self.post(endpoint, params=params)
diff --git a/backend/services/coingecko_client.py b/backend/services/coingecko_client.py
deleted file mode 100644
index 61ca5f7a..00000000
--- a/backend/services/coingecko_client.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import re
-import time
-
-import pandas as pd
-from pycoingecko import CoinGeckoAPI
-
-
-class CoinGeckoClient:
- def __init__(self):
- self.connector = CoinGeckoAPI()
-
- def get_all_coins_df(self):
- coin_list = self.connector.get_coins_list()
- return pd.DataFrame(coin_list)
-
- def get_all_coins_markets_df(self):
- coin_list = self.connector.get_coins_markets(vs_currency="USD")
- return pd.DataFrame(coin_list)
-
- def get_coin_tickers_by_id(self, coin_id: str):
- coin_tickers = self.connector.get_coin_ticker_by_id(id=coin_id)
- coin_tickers_df = pd.DataFrame(coin_tickers["tickers"])
- coin_tickers_df["token_id"] = coin_id
- return coin_tickers_df
-
- def get_coin_tickers_by_id_list(self, coins_id: list):
- dfs = []
- for coin_id in coins_id:
- df = self.get_coin_tickers_by_id(coin_id)
- dfs.append(df)
- time.sleep(1)
-
- coin_tickers_df = pd.concat(dfs)
- coin_tickers_df["exchange"] = coin_tickers_df["market"].apply(
- lambda x: re.sub("Exchange", "", x["name"]))
- coin_tickers_df.drop(columns="market", inplace=True)
- coin_tickers_df["trading_pair"] = coin_tickers_df.base + "-" + coin_tickers_df.target
- return coin_tickers_df
-
- def get_all_exchanges_df(self):
- exchanges_list = self.connector.get_exchanges_list()
- return pd.DataFrame(exchanges_list)
-
- def get_exchanges_markets_info_by_id_list(self, exchanges_id: list):
- dfs = []
- for exchange_id in exchanges_id:
- df = pd.DataFrame(self.connector.get_exchanges_by_id(exchange_id)["tickers"])
- dfs.append(df)
- exchanges_spreads_df = pd.concat(dfs)
- exchanges_spreads_df["exchange"] = exchanges_spreads_df["market"].apply(
- lambda x: re.sub("Exchange", "", x["name"]))
- exchanges_spreads_df.drop(columns="market", inplace=True)
- exchanges_spreads_df["trading_pair"] = exchanges_spreads_df.base + "-" + exchanges_spreads_df.target
- return exchanges_spreads_df
diff --git a/backend/services/miner_client.py b/backend/services/miner_client.py
deleted file mode 100644
index aff73cdf..00000000
--- a/backend/services/miner_client.py
+++ /dev/null
@@ -1,62 +0,0 @@
-import pandas as pd
-import requests
-from glom import glom
-
-
-class MinerClient:
- MARKETS_ENDPOINT = "https://api.hummingbot.io/bounty/markets"
-
- @staticmethod
- def reward_splitter(base, reward_dict):
- tmp = {"rewards_HBOT": 0, "rewards_STABLE": 0, "rewards_base": 0, }
- if "HBOT" in reward_dict:
- tmp["rewards_HBOT"] += reward_dict["HBOT"]
- if "USDC" in reward_dict:
- tmp["rewards_STABLE"] += reward_dict["USDC"]
- if "USDT" in reward_dict:
- tmp["rewards_STABLE"] += reward_dict["USDT"]
- if base in reward_dict:
- tmp["rewards_base"] += reward_dict[base]
-
- return pd.Series(tmp, dtype=float)
-
- @staticmethod
- def exchange_coingecko_id(exchange: str):
- converter = {
- "kucoin": "kucoin",
- "binance": "binance",
- "gateio": "gate",
- "ascendex": "bitmax"
- }
- return converter.get(exchange, None)
-
- def get_miner_stats_df(self):
- miner_data = requests.get(self.MARKETS_ENDPOINT).json()
- spec = {
- 'market_id': ('markets', ['market_id']),
- 'trading_pair': ('markets', ['trading_pair']),
- 'exchange': ('markets', ['exchange_name']),
- 'base': ('markets', ['base_asset']),
- 'quote': ('markets', ['quote_asset']),
- 'start_timestamp': ('markets', [("active_bounty_periods", ['start_timestamp'])]),
- 'end_timestamp': ('markets', [("active_bounty_periods", ['end_timestamp'])]),
- 'budget': ('markets', [("active_bounty_periods", ['budget'])]),
- 'spread_max': ('markets', [("active_bounty_periods", ['spread_max'])]),
- 'payout_asset': ('markets', [("active_bounty_periods", ['payout_asset'])]),
- 'return': ('markets', ['return']),
- 'last_snapshot_ts': ('markets', ['last_snapshot_ts']),
- 'hourly_payout_usd': ('markets', ['hourly_payout_usd']),
- 'bots': ('markets', ['bots']),
- 'last_hour_bots': ('markets', ['last_hour_bots']),
- 'filled_24h_volume': ('markets', ['filled_24h_volume']),
- # 'weekly_reward_in_usd': ('markets', ['weekly_reward_in_usd']),
- # 'weekly_reward': ('markets', ['weekly_reward']),
- 'market_24h_usd_volume': ('markets', ['market_24h_usd_volume'])
- }
-
- r = glom(miner_data, spec)
- df = pd.DataFrame(r)
- # df = pd.concat([df, df.apply(lambda x: self.reward_splitter(x.base, x.weekly_reward), axis=1)], axis=1)
- df["trading_pair"] = df.apply(lambda x: x.base + "-" + x.quote, axis=1)
- df["exchange_coingecko_id"] = df.apply(lambda x: self.exchange_coingecko_id(x.exchange), axis=1)
- return df
diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/backend/utils/file_templates.py b/backend/utils/file_templates.py
deleted file mode 100644
index 6a570b91..00000000
--- a/backend/utils/file_templates.py
+++ /dev/null
@@ -1,169 +0,0 @@
-from typing import Dict
-
-
-def directional_trading_controller_template(strategy_cls_name: str) -> str:
- strategy_config_cls_name = f"{strategy_cls_name}Config"
- sma_config_text = "{self.config.sma_length}"
- return f"""import time
-from typing import Optional
-
-import pandas as pd
-from pydantic import Field
-
-from hummingbot.strategy_v2.executors.position_executor.position_executor import PositionExecutor
-from hummingbot.strategy_v2.strategy_frameworks.data_types import OrderLevel
-from hummingbot.strategy_v2.strategy_frameworks.directional_trading.directional_trading_controller_base import (
- DirectionalTradingControllerBase,
- DirectionalTradingControllerConfigBase,
-)
-
-class {strategy_config_cls_name}(DirectionalTradingControllerConfigBase):
- strategy_name: str = "{strategy_cls_name.lower()}"
- sma_length: int = Field(default=20, ge=10, le=200)
- # ... Add more fields here
-
-
-class {strategy_cls_name}(DirectionalTradingControllerBase):
-
- def __init__(self, config: {strategy_config_cls_name}):
- super().__init__(config)
- self.config = config
-
-
- def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
- # If an executor has an active position, should we close it based on a condition. This feature is not available
- # for the backtesting yet
- return False
-
- def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
- # After finishing an order, the executor will be in cooldown for a certain amount of time.
- # This prevents the executor from creating a new order immediately after finishing one and execute a lot
- # of orders in a short period of time from the same side.
- if executor.close_timestamp and executor.close_timestamp + order_level.cooldown_time > time.time():
- return True
- return False
-
- def get_processed_data(self) -> pd.DataFrame:
- df = self.candles[0].candles_df
- df.ta.sma(length=self.config.sma_length, append=True)
- # ... Add more indicators here
- # ... Check https://github.com/twopirllc/pandas-ta#indicators-by-category for more indicators
- # ... Use help(ta.indicator_name) to get more info
-
- # Generate long and short conditions
- long_cond = (df['close'] > df[f'SMA_{sma_config_text}'])
- short_cond = (df['close'] < df[f'SMA_{sma_config_text}'])
-
- # Choose side
- df['signal'] = 0
- df.loc[long_cond, 'signal'] = 1
- df.loc[short_cond, 'signal'] = -1
- return df
-"""
-
-
-def get_optuna_suggest_str(field_name: str, properties: Dict):
- if field_name == "candles_config":
- return f"""{field_name}=[
- CandlesConfig(connector=exchange, trading_pair=trading_pair,
- interval="1h", max_records=1000000)
- ]"""
- if field_name == "strategy_name":
- return f"{field_name}='{properties.get('default', '_')}'"
- if field_name in ["order_levels", "trading_pair", "exchange"]:
- return f"{field_name}={field_name}"
- if field_name == "position_mode":
- return f"{field_name}=PositionMode.HEDGE"
- if field_name == "leverage":
- return f"{field_name}=10"
-
- if properties["type"] == "number":
- optuna_trial_str = f"trial.suggest_float('{field_name}', {properties.get('minimum', '_')}, {properties.get('maximum', '_')}, step=0.01)"
- elif properties["type"] == "integer":
- optuna_trial_str = f"trial.suggest_int('{field_name}', {properties.get('minimum', '_')}, {properties.get('maximum', '_')})"
- elif properties["type"] == "string":
- optuna_trial_str = f"trial.suggest_categorical('{field_name}', ['{properties.get('default', '_')}',])"
- else:
- raise Exception(f"Unknown type {properties['type']} for field {field_name}")
- return f"{field_name}={optuna_trial_str}"
-
-
-def strategy_optimization_template(strategy_info: dict):
- strategy_cls = strategy_info["class"]
- strategy_config = strategy_info["config"]
- strategy_module = strategy_info["module"]
- field_schema = strategy_config.schema()["properties"]
- fields_str = [get_optuna_suggest_str(field_name, properties) for field_name, properties in field_schema.items()]
- fields_str = "".join([f" {field_str},\n" for field_str in fields_str])
- return f"""import traceback
-from decimal import Decimal
-
-from hummingbot.core.data_type.common import PositionMode, TradeType, OrderType
-from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
-from hummingbot.strategy_v2.strategy_frameworks.data_types import TripleBarrierConf, OrderLevel
-from hummingbot.strategy_v2.strategy_frameworks.directional_trading import DirectionalTradingBacktestingEngine
-from hummingbot.strategy_v2.utils.config_encoder_decoder import ConfigEncoderDecoder
-from optuna import TrialPruned
-
-from quants_lab.controllers.{strategy_module} import {strategy_cls.__name__}, {strategy_config.__name__}
-
-
-def objective(trial):
- try:
- # General configuration for the backtesting
- exchange = "binance_perpetual"
- trading_pair = "BTC-USDT"
- start = "2023-01-01"
- end = "2024-01-01"
- initial_portfolio_usd = 1000.0
- trade_cost = 0.0006
-
- # The definition of order levels is not so necessary for directional strategies now but let's you customize the
- # amounts for going long or short, the cooldown time between orders and the triple barrier configuration
- stop_loss = trial.suggest_float('stop_loss', 0.01, 0.02, step=0.01)
- take_profit = trial.suggest_float('take_profit', 0.01, 0.05, step=0.01)
- time_limit = trial.suggest_int('time_limit', 60 * 60 * 12, 60 * 60 * 24)
-
- triple_barrier_conf = TripleBarrierConf(
- stop_loss=Decimal(stop_loss), take_profit=Decimal(take_profit),
- time_limit=time_limit,
- trailing_stop_activation_price_delta=Decimal("0.008"), # It's not working yet with the backtesting engine
- trailing_stop_trailing_delta=Decimal("0.004"),
- )
-
- order_levels = [
- OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(50),
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
- OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(50),
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
- ]
- config = {strategy_config.__name__}(
-{fields_str}
- )
- controller = {strategy_cls.__name__}(config=config)
- engine = DirectionalTradingBacktestingEngine(controller=controller)
- engine.load_controller_data("./data/candles")
- backtesting_results = engine.run_backtesting(initial_portfolio_usd=initial_portfolio_usd, trade_cost=trade_cost,
- start=start, end=end)
-
- strategy_analysis = backtesting_results["results"]
- encoder_decoder = ConfigEncoderDecoder(TradeType, OrderType, PositionMode)
-
- trial.set_user_attr("net_pnl_quote", strategy_analysis["net_pnl_quote"])
- trial.set_user_attr("net_pnl_pct", strategy_analysis["net_pnl"])
- trial.set_user_attr("max_drawdown_usd", strategy_analysis["max_drawdown_usd"])
- trial.set_user_attr("max_drawdown_pct", strategy_analysis["max_drawdown_pct"])
- trial.set_user_attr("sharpe_ratio", strategy_analysis["sharpe_ratio"])
- trial.set_user_attr("accuracy", strategy_analysis["accuracy"])
- trial.set_user_attr("total_positions", strategy_analysis["total_positions"])
- trial.set_user_attr("profit_factor", strategy_analysis["profit_factor"])
- trial.set_user_attr("duration_in_hours", strategy_analysis["duration_minutes"] / 60)
- trial.set_user_attr("avg_trading_time_in_hours", strategy_analysis["avg_trading_time_minutes"] / 60)
- trial.set_user_attr("win_signals", strategy_analysis["win_signals"])
- trial.set_user_attr("loss_signals", strategy_analysis["loss_signals"])
- trial.set_user_attr("config", encoder_decoder.encode(config.dict()))
- return strategy_analysis["net_pnl"]
- except Exception as e:
- traceback.print_exc()
- raise TrialPruned()
- """
diff --git a/backend/utils/optuna_database_manager.py b/backend/utils/optuna_database_manager.py
deleted file mode 100644
index f6cc229c..00000000
--- a/backend/utils/optuna_database_manager.py
+++ /dev/null
@@ -1,285 +0,0 @@
-import json
-import os
-from typing import Optional
-
-import pandas as pd
-from sqlalchemy import create_engine, text
-from sqlalchemy.orm import sessionmaker
-
-
-class OptunaDBManager:
- def __init__(self, db_name, db_root_path: Optional[str]):
- db_root_path = db_root_path or "data/backtesting"
- self.db_name = db_name
- self.db_path = f'sqlite:///{os.path.join(db_root_path, db_name)}'
- self.engine = create_engine(self.db_path, connect_args={'check_same_thread': False})
- self.session_maker = sessionmaker(bind=self.engine)
-
- @property
- def status(self):
- try:
- with self.session_maker() as session:
- query = 'SELECT * FROM trials WHERE state = "COMPLETE"'
- completed_trials = pd.read_sql_query(text(query), session.connection())
- if len(completed_trials) > 0:
- # TODO: improve error handling, think what to do with other cases
- return "OK"
- else:
- return "No records found in the trials table with completed state"
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def tables(self):
- return self._get_tables()
-
- def _get_tables(self):
- try:
- with self.session_maker() as session:
- query = "SELECT name FROM sqlite_master WHERE type='table';"
- tables = pd.read_sql_query(text(query), session.connection())
- return tables["name"].tolist()
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def trials(self):
- return self._get_trials_table()
-
- def _get_trials_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM trials"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def studies(self):
- return self._get_studies_table()
-
- def _get_studies_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM studies"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def trial_params(self):
- return self._get_trial_params_table()
-
- def _get_trial_params_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM trial_params"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def trial_values(self):
- return self._get_trial_values_table()
-
- def _get_trial_values_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM trial_values"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def trial_system_attributes(self):
- return self._get_trial_system_attributes_table()
-
- def _get_trial_system_attributes_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM trial_system_attributes"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def version_info(self):
- return self._get_version_info_table()
-
- def _get_version_info_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM version_info"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def study_directions(self):
- return self._get_study_directions_table()
-
- def _get_study_directions_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM study_directions"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def study_user_attributes(self):
- return self._get_study_user_attributes_table()
-
- def _get_study_user_attributes_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM study_user_attributes"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def study_system_attributes(self):
- return self._get_study_system_attributes_table()
-
- def _get_study_system_attributes_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM study_system_attributes"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def trial_user_attributes(self):
- return self._get_trial_user_attributes_table()
-
- def _get_trial_user_attributes_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM trial_user_attributes"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def trial_intermediate_values(self):
- return self._get_trial_intermediate_values_table()
-
- def _get_trial_intermediate_values_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM trial_intermediate_values"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def trial_heartbeats(self):
- return self._get_trial_heartbeats_table()
-
- def _get_trial_heartbeats_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM trial_heartbeats"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def alembic_version(self):
- return self._get_alembic_version_table()
-
- def _get_alembic_version_table(self):
- try:
- with self.session_maker() as session:
- df = pd.read_sql_query(text("SELECT * FROM alembic_version"), session.connection())
- return df
- except Exception as e:
- return f"Error: {str(e)}"
-
- @property
- def merged_df(self):
- return self._get_merged_df()
-
- @staticmethod
- def _add_hovertext(x):
- summary_label = (f"Trial ID: {x['trial_id']}
"
- f"Study: {x['study_name']}
"
- f"--------------------
"
- f"Accuracy: {100 * x['accuracy']:.2f} %
"
- f"Avg Trading Time in Hours: {x['avg_trading_time_in_hours']:.2f}
"
- f"Duration in Hours: {x['duration_in_hours']:.2f}
"
- f"Loss Signals: {x['loss_signals']}
"
- f"Max Drawdown [%]: {100 * x['max_drawdown_pct']:.2f} %
"
- f"Max Drawdown [USD]: $ {x['max_drawdown_usd']:.2f}
"
- f"Net Profit [%]: {100 * x['net_pnl_pct']:.2f} %
"
- f"Net Profit [$]: $ {x['net_pnl_quote']:.2f}
"
- f"Profit Factor: {x['profit_factor']:.2f}
"
- f"Sharpe Ratio: {x['sharpe_ratio']:.4f}
"
- f"Total Positions: {x['total_positions']}
"
- f"Win Signals: {x['win_signals']}
"
- f"Trial value: {x['value']}
"
- f"Direction: {x['direction']}
"
- )
- return summary_label
-
- def _get_merged_df(self):
- float_cols = ["accuracy", "avg_trading_time_in_hours", "duration_in_hours", "max_drawdown_pct", "max_drawdown_usd",
- "net_pnl_pct", "net_pnl_quote", "profit_factor", "sharpe_ratio", "value"]
- int_cols = ["loss_signals", "total_positions", "win_signals"]
- merged_df = self.trials\
- .merge(self.studies, on="study_id")\
- .merge(pd.pivot(self.trial_user_attributes, index="trial_id", columns="key", values="value_json"),
- on="trial_id")\
- .merge(self.trial_values, on="trial_id")\
- .merge(self.study_directions, on="study_id")
- merged_df[float_cols] = merged_df[float_cols].astype("float")
- merged_df[int_cols] = merged_df[int_cols].astype("int64")
- merged_df["hover_text"] = merged_df.apply(self._add_hovertext, axis=1)
- return merged_df
-
- def load_studies(self):
- df = self.merged_df
- study_name_col = 'study_name'
- trial_id_col = 'trial_id'
- nested_dict = {}
- for _, row in df.iterrows():
- study_name = row[study_name_col]
- trial_id = row[trial_id_col]
- data_dict = row.drop([study_name_col, trial_id_col]).to_dict()
- if study_name not in nested_dict:
- nested_dict[study_name] = {}
- nested_dict[study_name][trial_id] = data_dict
- return nested_dict
-
- def load_params(self):
- trial_id_col = 'trial_id'
- param_name_col = 'param_name'
- param_value_col = 'param_value'
- distribution_json_col = 'distribution_json'
- nested_dict = {}
- for _, row in self.trial_params.iterrows():
- trial_id = row[trial_id_col]
- param_name = row[param_name_col]
- param_value = row[param_value_col]
- distribution_json = row[distribution_json_col]
-
- if trial_id not in nested_dict:
- nested_dict[trial_id] = {}
-
- dist_json = json.loads(distribution_json)
- default_step = None
- default_low = None
- default_high = None
- default_log = None
-
- nested_dict[trial_id][param_name] = {
- 'param_name': param_name,
- 'param_value': param_value,
- 'step': dist_json["attributes"].get("step", default_step),
- 'low': dist_json["attributes"].get("low", default_low),
- 'high': dist_json["attributes"].get("high", default_high),
- 'log': dist_json["attributes"].get("log", default_log),
- }
- return nested_dict
diff --git a/backend/utils/os_utils.py b/backend/utils/os_utils.py
deleted file mode 100644
index cd42a69c..00000000
--- a/backend/utils/os_utils.py
+++ /dev/null
@@ -1,168 +0,0 @@
-import glob
-import importlib.util
-import inspect
-import os
-import subprocess
-
-import pandas as pd
-import yaml
-from hummingbot.strategy_v2.controllers.directional_trading_controller_base import (
- DirectionalTradingControllerBase,
- DirectionalTradingControllerConfigBase,
-)
-from hummingbot.strategy_v2.controllers.market_making_controller_base import (
- MarketMakingControllerBase,
- MarketMakingControllerConfigBase,
-)
-
-
-def remove_files_from_directory(directory: str):
- for file in os.listdir(directory):
- os.remove(f"{directory}/{file}")
-
-
-def remove_file(file_path: str):
- os.remove(file_path)
-
-
-def remove_directory(directory: str):
- process = subprocess.Popen(f"rm -rf {directory}", shell=True)
- process.wait()
-
-
-def dump_dict_to_yaml(data_dict, filename):
- with open(filename, 'w') as file:
- yaml.dump(data_dict, file)
-
-
-def read_yaml_file(file_path):
- with open(file_path, 'r') as file:
- data = yaml.safe_load(file)
- return data
-
-
-def directory_exists(directory: str):
- return os.path.exists(directory)
-
-
-def save_file(name: str, content: str, path: str):
- complete_file_path = os.path.join(path, name)
- os.makedirs(path, exist_ok=True)
- with open(complete_file_path, "w") as file:
- file.write(content)
-
-
-def load_file(path: str) -> str:
- try:
- with open(path, 'r') as file:
- contents = file.read()
- return contents
- except FileNotFoundError:
- print(f"File '{path}' not found.")
- return ""
- except IOError:
- print(f"Error reading file '{path}'.")
- return ""
-
-
-def get_directories_from_directory(directory: str) -> list:
- directories = glob.glob(directory + "/**/")
- return directories
-
-
-def get_python_files_from_directory(directory: str) -> list:
- py_files = glob.glob(directory + "/**/*.py", recursive=True)
- py_files = [path for path in py_files if not path.endswith("__init__.py")]
- return py_files
-
-
-def get_log_files_from_directory(directory: str) -> list:
- log_files = glob.glob(directory + "/**/*.log*", recursive=True)
- return log_files
-
-
-def get_yml_files_from_directory(directory: str) -> list:
- yml = glob.glob(directory + "/**/*.yml", recursive=True)
- return yml
-
-
-def load_controllers(path):
- controllers = {}
- for filename in os.listdir(path):
- if filename.endswith('.py') and "__init__" not in filename:
- module_name = filename[:-3] # strip the .py to get the module name
- controllers[module_name] = {"module": module_name}
- file_path = os.path.join(path, filename)
- spec = importlib.util.spec_from_file_location(module_name, file_path)
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
- for name, cls in inspect.getmembers(module, inspect.isclass):
- if issubclass(cls, DirectionalTradingControllerBase) and cls is not DirectionalTradingControllerBase:
- controllers[module_name]["class"] = cls
- controllers[module_name]["type"] = "directional_trading"
- if issubclass(cls, DirectionalTradingControllerConfigBase) and cls is not DirectionalTradingControllerConfigBase:
- controllers[module_name]["config"] = cls
- if issubclass(cls, MarketMakingControllerBase) and cls is not MarketMakingControllerBase:
- controllers[module_name]["class"] = cls
- controllers[module_name]["type"] = "market_making"
- if issubclass(cls, MarketMakingControllerConfigBase) and cls is not MarketMakingControllerConfigBase:
- controllers[module_name]["config"] = cls
- return controllers
-
-
-def get_bots_data_paths():
- root_directory = "hummingbot_files/bots"
- bots_data_paths = {"General / Uploaded data": "data"}
- reserved_word = "hummingbot-"
- # Walk through the directory tree
- for dirpath, dirnames, filenames in os.walk(root_directory):
- for dirname in dirnames:
- if dirname == "data":
- parent_folder = os.path.basename(dirpath)
- if parent_folder.startswith(reserved_word):
- bots_data_paths[parent_folder] = os.path.join(dirpath, dirname)
- if "dashboard" in bots_data_paths:
- del bots_data_paths["dashboard"]
- data_sources = {key: value for key, value in bots_data_paths.items() if value is not None}
- return data_sources
-
-
-def get_databases():
- databases = {}
- bots_data_paths = get_bots_data_paths()
- for source_name, source_path in bots_data_paths.items():
- sqlite_files = {}
- for db_name in os.listdir(source_path):
- if db_name.endswith(".sqlite"):
- sqlite_files[db_name] = os.path.join(source_path, db_name)
- databases[source_name] = sqlite_files
- if len(databases) > 0:
- return {key: value for key, value in databases.items() if value}
- else:
- return None
-
-
-def get_function_from_file(file_path: str, function_name: str):
- # Create a module specification from the file path and load it
- spec = importlib.util.spec_from_file_location("module.name", file_path)
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
-
- # Get the function from the module
- function = getattr(module, function_name)
- return function
-
-
-def execute_bash_command(command: str, shell: bool = True, wait: bool = False):
- process = subprocess.Popen(command, shell=shell)
- if wait:
- process.wait()
-
-
-def safe_read_csv(path):
- try:
- df = pd.read_csv(path)
- except pd.errors.ParserError as e:
- print("Error occurred while reading CSV:", str(e))
- df = pd.DataFrame()
- return df
diff --git a/backend/utils/performance_data_source.py b/backend/utils/performance_data_source.py
deleted file mode 100755
index 30e7bfd8..00000000
--- a/backend/utils/performance_data_source.py
+++ /dev/null
@@ -1,197 +0,0 @@
-import json
-from typing import Any, Dict, List
-
-import numpy as np
-import pandas as pd
-from hummingbot.core.data_type.common import TradeType
-from hummingbot.strategy_v2.models.base import RunnableStatus
-from hummingbot.strategy_v2.models.executors import CloseType
-from hummingbot.strategy_v2.models.executors_info import ExecutorInfo
-
-
-class PerformanceDataSource:
- def __init__(self,
- checkpoint_data: Dict[str, Any]):
- self.checkpoint_data = checkpoint_data
- self.executors_dict = self.checkpoint_data["executors"].copy()
- self.orders = self.load_orders()
- self.controllers_df = self.load_controllers()
- self.executors_with_orders = self.get_executors_with_orders(self.get_executors_df(), self.orders)
-
- def load_orders(self):
- """
- Load the orders data from the checkpoint.
- """
- orders = self.checkpoint_data["orders"].copy()
- orders = pd.DataFrame(orders)
- return orders
-
- def load_trade_fill(self):
- trade_fill = self.checkpoint_data["trade_fill"].copy()
- trade_fill = pd.DataFrame(trade_fill)
- trade_fill["timestamp"] = trade_fill["timestamp"].apply(lambda x: self.ensure_timestamp_in_seconds(x))
- trade_fill["datetime"] = pd.to_datetime(trade_fill.timestamp, unit="s")
- return trade_fill
-
- def load_controllers(self):
- controllers = self.checkpoint_data["controllers"].copy()
- controllers = pd.DataFrame(controllers)
- controllers["config"] = controllers["config"].apply(lambda x: json.loads(x))
- controllers["datetime"] = pd.to_datetime(controllers.timestamp, unit="s")
- return controllers
-
- @property
- def controllers_dict(self):
- return {controller["id"]: controller["config"] for controller in self.controllers_df.to_dict(orient="records")}
-
- def get_executors_df(self, executors_filter: Dict[str, Any] = None, apply_executor_data_types: bool = False):
- executors_df = pd.DataFrame(self.executors_dict)
- executors_df["custom_info"] = executors_df["custom_info"].apply(
- lambda x: json.loads(x) if isinstance(x, str) else x
- )
- executors_df["config"] = executors_df["config"].apply(lambda x: json.loads(x) if isinstance(x, str) else x)
- executors_df["timestamp"] = executors_df["timestamp"].apply(lambda x: self.ensure_timestamp_in_seconds(x))
- executors_df["close_timestamp"] = executors_df["close_timestamp"].apply(
- lambda x: self.ensure_timestamp_in_seconds(x)
- )
- executors_df.sort_values("close_timestamp", inplace=True)
- executors_df["trading_pair"] = executors_df["config"].apply(lambda x: x["trading_pair"])
- executors_df["exchange"] = executors_df["config"].apply(lambda x: x["connector_name"])
- executors_df["status"] = executors_df["status"].astype(int)
- executors_df["level_id"] = executors_df["config"].apply(lambda x: x.get("level_id"))
- executors_df["bep"] = executors_df["custom_info"].apply(lambda x: x["current_position_average_price"])
- executors_df["order_ids"] = executors_df["custom_info"].apply(lambda x: x.get("order_ids"))
- executors_df["close_price"] = executors_df["custom_info"].apply(
- lambda x: x.get("close_price", x["current_position_average_price"]))
- executors_df["sl"] = executors_df["config"].apply(lambda x: x.get("stop_loss")).fillna(0)
- executors_df["tp"] = executors_df["config"].apply(lambda x: x.get("take_profit")).fillna(0)
- executors_df["tl"] = executors_df["config"].apply(lambda x: x.get("time_limit")).fillna(0)
- executors_df["close_type_name"] = executors_df["close_type"].apply(lambda x: self.get_enum_by_value(CloseType, x).name)
-
- controllers = self.controllers_df.copy()
- controllers.drop(columns=["controller_id"], inplace=True)
- controllers.rename(columns={
- "config": "controller_config",
- "type": "controller_type",
- "id": "controller_id"
- }, inplace=True)
-
- executors_df = executors_df.merge(controllers[["controller_id", "controller_type", "controller_config"]],
- on="controller_id", how="left")
- if apply_executor_data_types:
- executors_df = self.apply_executor_data_types(executors_df)
- if executors_filter is not None:
- executors_df = self.filter_executors(executors_df, executors_filter)
- return executors_df
-
- def apply_executor_data_types(self, executors):
- executors["status"] = executors["status"].apply(lambda x: self.get_enum_by_value(RunnableStatus, int(x)))
- executors["side"] = executors["config"].apply(lambda x: self.get_enum_by_value(TradeType, int(x["side"])))
- executors["close_type"] = executors["close_type"].apply(lambda x: self.get_enum_by_value(CloseType, int(x)))
- executors["datetime"] = pd.to_datetime(executors.timestamp, unit="s")
- executors["close_datetime"] = pd.to_datetime(executors["close_timestamp"], unit="s")
- return executors
-
- @staticmethod
- def remove_executor_data_types(executors):
- executors["status"] = executors["status"].apply(lambda x: x.value)
- executors["side"] = executors["side"].apply(lambda x: x.value)
- executors["close_type"] = executors["close_type"].apply(lambda x: x.value)
- executors.drop(columns=["datetime", "close_datetime"], inplace=True)
- return executors
-
- @staticmethod
- def get_executors_with_orders(executors_df: pd.DataFrame, orders: pd.DataFrame):
- df = (executors_df[["id", "order_ids"]]
- .rename(columns={"id": "executor_id", "order_ids": "order_id"})
- .explode("order_id"))
- exec_with_orders = df.merge(orders, left_on="order_id", right_on="client_order_id", how="inner")
- exec_with_orders = exec_with_orders[exec_with_orders["last_status"].isin(["SellOrderCompleted",
- "BuyOrderCompleted"])]
- return exec_with_orders[["executor_id", "order_id", "last_status", "last_update_timestamp",
- "price", "amount", "position"]]
-
- def get_executor_info_list(self,
- executors_filter: Dict[str, Any] = None) -> List[ExecutorInfo]:
- required_columns = [
- "id", "timestamp", "type", "close_timestamp", "close_type", "status", "controller_type",
- "net_pnl_pct", "net_pnl_quote", "cum_fees_quote", "filled_amount_quote",
- "is_active", "is_trading", "controller_id", "side", "config", "custom_info", "exchange", "trading_pair"
- ]
- executors_df = self.get_executors_df(executors_filter=executors_filter,
- apply_executor_data_types=True
- )[required_columns].copy()
- executors_df = executors_df[executors_df["net_pnl_quote"] != 0]
- executor_info_list = executors_df.apply(lambda row: ExecutorInfo(**row.to_dict()), axis=1).tolist()
- return executor_info_list
-
- def get_executor_dict(self,
- executors_filter: Dict[str, Any] = None,
- apply_executor_data_types: bool = False,
- remove_special_fields: bool = False) -> List[dict]:
- executors_df = self.get_executors_df(executors_filter,
- apply_executor_data_types=apply_executor_data_types).copy()
- if remove_special_fields:
- executors_df = self.remove_executor_data_types(executors_df)
- return executors_df.to_dict(orient="records")
-
- def get_executors_by_controller_type(self,
- executors_filter: Dict[str, Any] = None) -> Dict[str, pd.DataFrame]:
- executors_by_controller_type = {}
- executors_df = self.get_executors_df(executors_filter).copy()
- for controller_type in executors_df["controller_type"].unique():
- executors_by_controller_type[controller_type] = executors_df[
- executors_df["controller_type"] == controller_type
- ]
- return executors_by_controller_type
-
- @staticmethod
- def filter_executors(executors_df: pd.DataFrame,
- filters: Dict[str, List[Any]]):
- filter_condition = np.array([True] * len(executors_df))
- for key, value in filters.items():
- if isinstance(value, list) and len(value) > 0:
- filter_condition &= np.array(executors_df[key].isin(value))
- elif key == "start_time":
- filter_condition &= np.array(executors_df["timestamp"] >= value - 60)
- elif key == "close_type_name":
- filter_condition &= np.array(executors_df["close_type_name"] == value)
- elif key == "end_time":
- filter_condition &= np.array(executors_df["close_timestamp"] <= value + 60)
-
- return executors_df[filter_condition]
-
- @staticmethod
- def get_enum_by_value(enum_class, value):
- for member in enum_class:
- if member.value == value:
- return member
- raise ValueError(f"No enum member with value {value}")
-
- @staticmethod
- def ensure_timestamp_in_seconds(timestamp: float) -> float:
- """
- Ensure the given timestamp is in seconds.
-
- Args:
- - timestamp (int): The input timestamp which could be in seconds, milliseconds, or microseconds.
-
- Returns:
- - int: The timestamp in seconds.
-
- Raises:
- - ValueError: If the timestamp is not in a recognized format.
- """
- timestamp_int = int(float(timestamp))
- if timestamp_int >= 1e18: # Nanoseconds
- return timestamp_int / 1e9
- elif timestamp_int >= 1e15: # Microseconds
- return timestamp_int / 1e6
- elif timestamp_int >= 1e12: # Milliseconds
- return timestamp_int / 1e3
- elif timestamp_int >= 1e9: # Seconds
- return timestamp_int
- else:
- raise ValueError(
- "Timestamp is not in a recognized format. Must be in seconds, milliseconds, microseconds or "
- "nanoseconds.")
diff --git a/environment_conda.yml b/environment_conda.yml
index e62c4dd0..1a3f54c6 100644
--- a/environment_conda.yml
+++ b/environment_conda.yml
@@ -8,8 +8,10 @@ dependencies:
- pip
- pip:
- hummingbot
+ - hummingbot-api-client
+ - nest_asyncio
- pydantic
- - streamlit==1.40.0
+ - streamlit>=1.36.0
- watchdog
- python-dotenv
- plotly==5.24.1
@@ -20,8 +22,6 @@ dependencies:
- pandas_ta==0.3.14b
- pyyaml
- pathlib
- - st_pages
- - streamlit-elements==0.1.*
- streamlit-authenticator==0.3.2
- flake8
- isort
diff --git a/frontend/components/backtesting.py b/frontend/components/backtesting.py
index 8b342a08..9b0fbe51 100644
--- a/frontend/components/backtesting.py
+++ b/frontend/components/backtesting.py
@@ -25,7 +25,7 @@ def backtesting_section(inputs, backend_api_client):
start_datetime = datetime.combine(start_date, datetime.min.time())
end_datetime = datetime.combine(end_date, datetime.max.time())
try:
- backtesting_results = backend_api_client.run_backtesting(
+ backtesting_results = backend_api_client.backtesting.run_backtesting(
start_time=int(start_datetime.timestamp()),
end_time=int(end_datetime.timestamp()),
backtesting_resolution=backtesting_resolution,
diff --git a/frontend/components/bot_performance_card.py b/frontend/components/bot_performance_card.py
deleted file mode 100644
index a903e788..00000000
--- a/frontend/components/bot_performance_card.py
+++ /dev/null
@@ -1,345 +0,0 @@
-import pandas as pd
-from streamlit_elements import mui
-
-from frontend.components.dashboard import Dashboard
-from frontend.st_utils import get_backend_api_client
-
-TRADES_TO_SHOW = 5
-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):
- backend_api_client.stop_bot(bot_name)
-
-
-def archive_bot(bot_name):
- backend_api_client.stop_container(bot_name)
- backend_api_client.remove_container(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": 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 = get_backend_api_client()
-
- def _handle_stopped_row_selection(self, params, _):
- self._stopped_controller_config_selected = params
-
- def _handle_active_row_selection(self, params, _):
- self._active_controller_config_selected = params
-
- def _handle_errors_row_selection(self, params, _):
- self._error_controller_config_selected = params
-
- def stop_active_controllers(self, bot_name):
- for controller in self._active_controller_config_selected:
- self._backend_api_client.stop_controller_from_bot(bot_name, controller)
-
- def stop_errors_controllers(self, bot_name):
- for controller in self._error_controller_config_selected:
- self._backend_api_client.stop_controller_from_bot(bot_name, controller)
-
- def start_controllers(self, bot_name):
- for controller in self._stopped_controller_config_selected:
- self._backend_api_client.start_controller_from_bot(bot_name, controller)
-
- def __call__(self, bot_name: str):
- try:
- controller_configs = backend_api_client.get_all_configs_from_bot(bot_name)
- controller_configs = controller_configs if controller_configs else []
- bot_status = backend_api_client.get_bot_status(bot_name)
- # Controllers Table
- active_controllers_list = []
- stopped_controllers_list = []
- error_controllers_list = []
- total_global_pnl_quote = 0
- total_volume_traded = 0
- total_open_order_volume = 0
- total_imbalance = 0
- total_unrealized_pnl_quote = 0
- bot_data = bot_status.get("data")
- error_logs = bot_data.get("error_logs", [])
- general_logs = bot_data.get("general_logs", [])
- if bot_status.get("status") == "error":
- with mui.Card(key=self._key,
- sx={"display": "flex", "flexDirection": "column", "borderRadius": 2, "overflow": "auto"},
- elevation=2):
- mui.CardHeader(
- title=bot_name,
- subheader="Not Available",
- avatar=mui.Avatar("🤖", sx={"bgcolor": "red"}),
- className=self._draggable_class)
- mui.Alert(
- f"An error occurred while fetching bot status of the bot {bot_name}. Please check the bot client.",
- severity="error")
- else:
- is_running = bot_data.get("status") == "running"
- performance = bot_data.get("performance")
- if is_running:
- for controller, inner_dict in performance.items():
- controller_status = inner_dict.get("status")
- if controller_status == "error":
- error_controllers_list.append(
- {"id": controller, "error": inner_dict.get("error")})
- 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)
- global_pnl_quote = controller_performance.get("global_pnl_quote", 0)
- volume_traded = controller_performance.get("volume_traded", 0)
- open_order_volume = controller_performance.get("open_order_volume", 0)
- imbalance = controller_performance.get("inventory_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)
- failed = close_types.get("CloseType.FAILED", 0)
- close_types_str = f"TP: {tp} | SL: {sl} | TS: {ts} | TL: {time_limit} | ES: {refreshed} | F: {failed}"
- controller_info = {
- "id": controller,
- "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)
- else:
- active_controllers_list.append(controller_info)
- total_global_pnl_quote += global_pnl_quote
- total_volume_traded += volume_traded
- total_open_order_volume += open_order_volume
- total_imbalance += imbalance
- total_unrealized_pnl_quote += unrealized_pnl_quote
- total_global_pnl_pct = total_global_pnl_quote / total_volume_traded if total_volume_traded > 0 else 0
-
- if is_running:
- status = "Running"
- color = "green"
- else:
- status = "Stopped"
- color = "red"
-
- with mui.Card(key=self._key,
- sx={"display": "flex", "flexDirection": "column", "borderRadius": 2, "overflow": "auto"},
- elevation=2):
- mui.CardHeader(
- title=bot_name,
- subheader=status,
- avatar=mui.Avatar("🤖", sx={"bgcolor": color}),
- action=mui.IconButton(mui.icon.Stop,
- onClick=lambda: stop_bot(bot_name)) if is_running else mui.IconButton(
- mui.icon.Archive, onClick=lambda: archive_bot(bot_name)),
- className=self._draggable_class)
- if is_running:
- with mui.CardContent(sx={"flex": 1}):
- with mui.Grid(container=True, spacing=2, sx={"padding": "10px 15px 10px 15px"}):
- with mui.Grid(item=True, xs=2):
- 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("🏦 NET PNL", variant="h6")
- mui.Typography(f"$ {total_global_pnl_quote:.3f}", variant="h6",
- sx={"padding": "10px 15px 10px 15px"})
- with mui.Grid(item=True, xs=2):
- 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("📊 NET PNL (%)", variant="h6")
- mui.Typography(f"{total_global_pnl_pct:.3%}", variant="h6",
- sx={"padding": "10px 15px 10px 15px"})
- with mui.Grid(item=True, xs=2):
- 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("💸 Volume Traded", variant="h6")
- mui.Typography(f"$ {total_volume_traded:.2f}", variant="h6",
- sx={"padding": "10px 15px 10px 15px"})
- with mui.Grid(item=True, xs=2):
- 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("📖 Liquidity Placed", variant="h6")
- mui.Typography(f"$ {total_open_order_volume:.2f}", variant="h6",
- sx={"padding": "10px 15px 10px 15px"})
- with mui.Grid(item=True, xs=2):
- 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("💹 Unrealized PNL", variant="h6")
- mui.Typography(f"$ {total_unrealized_pnl_quote:.2f}", variant="h6",
- sx={"padding": "10px 15px 10px 15px"})
- with mui.Grid(item=True, xs=2):
- 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("📊 Imbalance", variant="h6")
- mui.Typography(f"$ {total_imbalance:.2f}", variant="h6",
- sx={"padding": "10px 15px 10px 15px"})
-
- with mui.Grid(container=True, spacing=1, sx={"padding": "10px 15px 10px 15px"}):
- with mui.Grid(item=True, xs=11):
- 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("🚀 Active Controllers", variant="h6")
- mui.DataGrid(
- rows=active_controllers_list,
- columns=self.DEFAULT_COLUMNS,
- autoHeight=True,
- density="compact",
- checkboxSelection=True,
- disableSelectionOnClick=True,
- onSelectionModelChange=self._handle_active_row_selection,
- hideFooter=True
- )
- with mui.Grid(item=True, xs=1):
- with mui.Button(onClick=lambda x: self.stop_active_controllers(bot_name),
- variant="outlined",
- color="warning",
- sx={"width": "100%", "height": "100%"}):
- mui.icon.AddCircleOutline()
- mui.Typography("Stop")
- if len(stopped_controllers_list) > 0:
- with mui.Grid(container=True, spacing=1, sx={"padding": "10px 15px 10px 15px"}):
- with mui.Grid(item=True, xs=11):
- 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("💤 Stopped Controllers", variant="h6")
- mui.DataGrid(
- rows=stopped_controllers_list,
- columns=self.DEFAULT_COLUMNS,
- autoHeight=True,
- density="compact",
- checkboxSelection=True,
- disableSelectionOnClick=True,
- onSelectionModelChange=self._handle_stopped_row_selection,
- hideFooter=True
- )
- with mui.Grid(item=True, xs=1):
- with mui.Button(onClick=lambda x: self.start_controllers(bot_name),
- variant="outlined",
- color="success",
- sx={"width": "100%", "height": "100%"}):
- mui.icon.AddCircleOutline()
- mui.Typography("Start")
- if len(error_controllers_list) > 0:
- with mui.Grid(container=True, spacing=1, sx={"padding": "10px 15px 10px 15px"}):
- with mui.Grid(item=True, xs=11):
- 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("💀 Controllers with errors", variant="h6")
- mui.DataGrid(
- rows=error_controllers_list,
- columns=self.DEFAULT_COLUMNS,
- autoHeight=True,
- density="compact",
- checkboxSelection=True,
- disableSelectionOnClick=True,
- onSelectionModelChange=self._handle_errors_row_selection,
- hideFooter=True
- )
- with mui.Grid(item=True, xs=1):
- with mui.Button(onClick=lambda x: self.stop_errors_controllers(bot_name),
- variant="outlined",
- color="warning",
- 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,
- sx={"display": "flex", "flexDirection": "column", "borderRadius": 2, "overflow": "auto"},
- elevation=2):
- mui.CardHeader(
- title=bot_name,
- subheader="Error",
- avatar=mui.Avatar("🤖", sx={"bgcolor": "red"}),
- action=mui.IconButton(mui.icon.Stop, onClick=lambda: stop_bot(bot_name)),
- className=self._draggable_class)
- with mui.CardContent(sx={"flex": 1}):
- mui.Typography("An error occurred while fetching bot status.",
- sx={"padding": "10px 15px 10px 15px"})
- mui.Typography(str(e), sx={"padding": "10px 15px 10px 15px"})
diff --git a/frontend/components/bots_file_explorer.py b/frontend/components/bots_file_explorer.py
deleted file mode 100644
index ea3593ee..00000000
--- a/frontend/components/bots_file_explorer.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from streamlit_elements import mui
-
-import constants
-from backend.utils.os_utils import (
- get_directories_from_directory,
- get_log_files_from_directory,
- get_python_files_from_directory,
- get_yml_files_from_directory,
-)
-from frontend.components.file_explorer_base import FileExplorerBase
-
-
-class BotsFileExplorer(FileExplorerBase):
- def add_tree_view(self):
- directory = constants.BOTS_FOLDER
- bots = [bot.split("/")[-2] for bot in get_directories_from_directory(directory) if
- "data_downloader" not in bot]
- with mui.lab.TreeView(defaultExpandIcon=mui.icon.ChevronRight, defaultCollapseIcon=mui.icon.ExpandMore,
- onNodeSelect=lambda event, node_id: self.set_selected_file(event, node_id)):
- for bot in bots:
- with mui.lab.TreeItem(nodeId=bot, label=f"🤖{bot}"):
- with mui.lab.TreeItem(nodeId=f"scripts_{bot}", label="🐍Scripts"):
- for file in get_python_files_from_directory(f"{directory}/{bot}/scripts"):
- mui.lab.TreeItem(nodeId=file, label=f"📄{file.split('/')[-1]}")
- with mui.lab.TreeItem(nodeId=f"strategies_{bot}", label="📜Strategies"):
- for file in get_yml_files_from_directory(f"{directory}/{bot}/conf/strategies"):
- mui.lab.TreeItem(nodeId=file, label=f"📄 {file.split('/')[-1]}")
- with mui.lab.TreeItem(nodeId=f"logs_{bot}", label="🗄️Logs"):
- for file in get_log_files_from_directory(f"{directory}/{bot}/logs"):
- mui.lab.TreeItem(nodeId=file, label=f"📄 {file.split('/')[-1]}")
diff --git a/frontend/components/card.py b/frontend/components/card.py
deleted file mode 100644
index a1b1da15..00000000
--- a/frontend/components/card.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from streamlit_elements import mui
-
-from frontend.components.dashboard import Dashboard
-
-
-class Card(Dashboard.Item):
- DEFAULT_CONTENT = (
- "This impressive paella is a perfect party dish and a fun meal to cook "
- "together with your guests. Add 1 cup of frozen peas along with the mussels, "
- "if you like."
- )
-
- def __call__(self, content):
- with mui.Card(key=self._key,
- sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"},
- elevation=1):
- mui.CardHeader(
- title="Shrimp and Chorizo Paella",
- subheader="September 14, 2016",
- avatar=mui.Avatar("R", sx={"bgcolor": "red"}),
- action=mui.IconButton(mui.icon.MoreVert),
- className=self._draggable_class,
- )
- mui.CardMedia(
- component="img",
- height=194,
- image="https://mui.com/static/images/cards/paella.jpg",
- alt="Paella dish",
- )
-
- with mui.CardContent(sx={"flex": 1}):
- mui.Typography(content)
-
- with mui.CardActions(disableSpacing=True):
- mui.IconButton(mui.icon.Favorite)
- mui.IconButton(mui.icon.Share)
diff --git a/frontend/components/config_loader.py b/frontend/components/config_loader.py
index 20f0da8b..2121bcdd 100644
--- a/frontend/components/config_loader.py
+++ b/frontend/components/config_loader.py
@@ -1,30 +1,158 @@
+import copy
import streamlit as st
+import nest_asyncio
from frontend.st_utils import get_backend_api_client
from frontend.utils import generate_random_name
+nest_asyncio.apply()
backend_api_client = get_backend_api_client()
def get_default_config_loader(controller_name: str):
- all_configs = backend_api_client.get_all_controllers_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
+ """
+ Load default configuration for a controller with proper session state isolation.
+ Uses controller-specific session state keys to prevent cross-contamination.
+ """
+ # Use controller-specific session state key to prevent cross-contamination
+ config_key = f"config_{controller_name}"
+ loader_key = f"config_loader_initialized_{controller_name}"
+
+ try:
+ all_configs = backend_api_client.controllers.list_controller_configs()
+ except Exception as e:
+ st.error(f"Failed to fetch controller configs: {e}")
+ all_configs = []
+
+ # Handle both old and new config format
+ existing_configs = []
+ for config in all_configs:
+ config_name = config.get("config_name", config.get("id", ""))
+ if config_name:
+ existing_configs.append(config_name.split("_")[0])
+
+ # Create default configuration with unique ID
+ default_dict = {
+ "id": generate_random_name(existing_configs),
+ "controller_name": controller_name
+ }
+
+ # Initialize controller-specific config if not exists
+ if config_key not in st.session_state:
+ st.session_state[config_key] = copy.deepcopy(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)
+ use_default_config = st.checkbox(
+ "Use default config",
+ value=st.session_state.get(f"use_default_{controller_name}", True),
+ key=f"use_default_{controller_name}"
+ )
with c2:
if not use_default_config:
- configs = [config for config in all_configs if config["controller_name"] == controller_name]
+ # Filter configs by controller name
+ configs = []
+ for config in all_configs:
+ config_data = config.get("config", config)
+ if config_data.get("controller_name") == controller_name:
+ configs.append(config)
+
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]
+ config_names = [config.get("config_name", config.get("id", "Unknown")) for config in configs]
+ selected_config_name = st.selectbox(
+ "Select a config",
+ config_names,
+ key=f"config_select_{controller_name}"
+ )
+
+ # Find the selected config
+ selected_config = None
+ for config in configs:
+ if config.get("config_name", config.get("id", "")) == selected_config_name:
+ selected_config = config
+ break
+
+ if selected_config:
+ # Use deep copy to prevent shared references
+ config_data = selected_config.get("config", selected_config)
+ st.session_state[config_key] = copy.deepcopy(config_data)
+ st.session_state[config_key]["id"] = selected_config_name.split("_")[0]
+ st.session_state[config_key]["controller_name"] = controller_name
else:
st.warning("No existing configs found for this controller.")
+
+ # Set legacy key for backward compatibility (but with deep copy)
+ st.session_state["default_config"] = copy.deepcopy(st.session_state[config_key])
+
+
+def get_controller_config(controller_name: str) -> dict:
+ """
+ Get the current configuration for a controller with proper isolation.
+ Returns a deep copy to prevent shared reference mutations.
+ """
+ config_key = f"config_{controller_name}"
+
+ if config_key not in st.session_state:
+ # Initialize with basic config if not found
+ existing_configs = []
+ try:
+ all_configs = backend_api_client.controllers.list_controller_configs()
+ for config in all_configs:
+ config_name = config.get("config_name", config.get("id", ""))
+ if config_name:
+ existing_configs.append(config_name.split("_")[0])
+ except Exception:
+ pass
+
+ default_dict = {
+ "id": generate_random_name(existing_configs),
+ "controller_name": controller_name
+ }
+ st.session_state[config_key] = copy.deepcopy(default_dict)
+
+ # Always return a deep copy to prevent mutations
+ return copy.deepcopy(st.session_state[config_key])
+
+
+def update_controller_config(controller_name: str, config_updates: dict) -> None:
+ """
+ Update the configuration for a controller with proper isolation.
+ Performs a deep copy of the updates to prevent shared references.
+ """
+ config_key = f"config_{controller_name}"
+
+ # Get current config or initialize if not exists
+ current_config = get_controller_config(controller_name)
+
+ # Deep copy the updates to prevent shared references
+ safe_updates = copy.deepcopy(config_updates)
+
+ # Update the config
+ current_config.update(safe_updates)
+
+ # Store the updated config
+ st.session_state[config_key] = current_config
+
+ # Update legacy key for backward compatibility
+ st.session_state["default_config"] = copy.deepcopy(current_config)
+
+
+def reset_controller_config(controller_name: str) -> None:
+ """
+ Reset the configuration for a controller, clearing all session state.
+ """
+ config_key = f"config_{controller_name}"
+ loader_key = f"config_loader_initialized_{controller_name}"
+
+ # Clear controller-specific state
+ st.session_state.pop(config_key, None)
+ st.session_state.pop(loader_key, None)
+
+ # Clear related UI state
+ st.session_state.pop(f"use_default_{controller_name}", None)
+ st.session_state.pop(f"config_select_{controller_name}", None)
+
+ # Clear legacy state if it matches this controller
+ if st.session_state.get("default_config", {}).get("controller_name") == controller_name:
+ st.session_state.pop("default_config", None)
diff --git a/frontend/components/controllers_file_explorer.py b/frontend/components/controllers_file_explorer.py
deleted file mode 100644
index 5939aa75..00000000
--- a/frontend/components/controllers_file_explorer.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from streamlit_elements import mui
-
-import constants
-from backend.utils.os_utils import load_controllers
-from frontend.components.file_explorer_base import FileExplorerBase
-
-
-class ControllersFileExplorer(FileExplorerBase):
- def add_tree_view(self):
- with mui.lab.TreeView(defaultExpandIcon=mui.icon.ChevronRight, defaultCollapseIcon=mui.icon.ExpandMore,
- onNodeSelect=lambda event, node_id: self.set_selected_file(event, node_id),
- defaultExpanded=["directional_strategies"]):
- available_controllers = load_controllers(constants.CONTROLLERS_PATH)
- with mui.lab.TreeItem(nodeId="directional_strategies", label="⚔️Directional Strategies"):
- for controller in available_controllers:
- if available_controllers[controller]["type"] == "directional_trading":
- mui.lab.TreeItem(nodeId=constants.CONTROLLERS_PATH + "/" + controller + ".py",
- label=f"🐍{controller}")
- with mui.lab.TreeItem(nodeId="market_making_strategies", label="🪙Market Making Strategies"):
- for controller in available_controllers:
- if available_controllers[controller]["type"] == "market_making":
- mui.lab.TreeItem(nodeId=constants.CONTROLLERS_PATH + "/" + controller + ".py",
- label=f"🐍{controller}")
diff --git a/frontend/components/dashboard.py b/frontend/components/dashboard.py
deleted file mode 100644
index 988878f3..00000000
--- a/frontend/components/dashboard.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from abc import ABC, abstractmethod
-from contextlib import contextmanager
-from uuid import uuid4
-
-from streamlit_elements import dashboard, mui
-
-
-class Dashboard:
- DRAGGABLE_CLASS = "draggable"
-
- def __init__(self):
- self._layout = []
-
- def _register(self, item):
- self._layout.append(item)
-
- @contextmanager
- def __call__(self, **props):
- # Draggable classname query selector.
- props["draggableHandle"] = f".{Dashboard.DRAGGABLE_CLASS}"
-
- with dashboard.Grid(self._layout, **props):
- yield
-
- class Item(ABC):
-
- def __init__(self, board, x, y, w, h, **item_props):
- self._key = str(uuid4())
- self._draggable_class = Dashboard.DRAGGABLE_CLASS
- self._dark_mode = True
- board._register(dashboard.Item(self._key, x, y, w, h, **item_props))
-
- def _switch_theme(self):
- self._dark_mode = not self._dark_mode
-
- @contextmanager
- def title_bar(self, padding="5px 15px 5px 15px", dark_switcher=True):
- with mui.Stack(
- className=self._draggable_class,
- alignItems="center",
- direction="row",
- spacing=1,
- sx={
- "padding": padding,
- "borderBottom": 1,
- "borderColor": "divider",
- },
- ):
- yield
-
- if dark_switcher:
- if self._dark_mode:
- mui.IconButton(mui.icon.DarkMode, onClick=self._switch_theme)
- else:
- mui.IconButton(mui.icon.LightMode, sx={"color": "#ffc107"}, onClick=self._switch_theme)
-
- @abstractmethod
- def __call__(self):
- """Show elements."""
- raise NotImplementedError
diff --git a/frontend/components/datagrid.py b/frontend/components/datagrid.py
deleted file mode 100644
index 88e6be1d..00000000
--- a/frontend/components/datagrid.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import json
-
-from streamlit_elements import mui
-
-from .dashboard import Dashboard
-
-
-class DataGrid(Dashboard.Item):
- DEFAULT_COLUMNS = [
- {"field": 'id', "headerName": 'ID', "width": 90},
- {"field": 'firstName', "headerName": 'First name', "width": 150, "editable": True, },
- {"field": 'lastName', "headerName": 'Last name', "width": 150, "editable": True, },
- {"field": 'age', "headerName": 'Age', "type": 'number', "width": 110, "editable": True, },
- ]
- DEFAULT_ROWS = [
- {"id": 1, "lastName": 'Snow', "firstName": 'Jon', "age": 35},
- {"id": 2, "lastName": 'Lannister', "firstName": 'Cersei', "age": 42},
- {"id": 3, "lastName": 'Lannister', "firstName": 'Jaime', "age": 45},
- {"id": 4, "lastName": 'Stark', "firstName": 'Arya', "age": 16},
- {"id": 5, "lastName": 'Targaryen', "firstName": 'Daenerys', "age": None},
- {"id": 6, "lastName": 'Melisandre', "firstName": None, "age": 150},
- {"id": 7, "lastName": 'Clifford', "firstName": 'Ferrara', "age": 44},
- {"id": 8, "lastName": 'Frances', "firstName": 'Rossini', "age": 36},
- {"id": 9, "lastName": 'Roxie', "firstName": 'Harvey', "age": 65},
- ]
-
- def _handle_edit(self, params):
- print(params)
-
- def __call__(self, json_data):
- try:
- data = json.loads(json_data)
- except json.JSONDecodeError:
- data = self.DEFAULT_ROWS
-
- 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.icon.ViewCompact()
- mui.Typography("Data grid")
-
- with mui.Box(sx={"flex": 1, "minHeight": 0}):
- mui.DataGrid(
- columns=self.DEFAULT_COLUMNS,
- rows=data,
- pageSize=5,
- rowsPerPageOptions=[5],
- checkboxSelection=True,
- disableSelectionOnClick=True,
- onCellEditCommit=self._handle_edit,
- )
diff --git a/frontend/components/dca_distribution.py b/frontend/components/dca_distribution.py
index fe3867b5..3371ddea 100644
--- a/frontend/components/dca_distribution.py
+++ b/frontend/components/dca_distribution.py
@@ -1,13 +1,18 @@
import streamlit as st
+from frontend.components.config_loader import get_controller_config
from frontend.components.st_inputs import distribution_inputs, get_distribution, normalize
-def get_dca_distribution_inputs():
+def get_dca_distribution_inputs(controller_name: str = None):
with st.expander("DCA Builder", expanded=True):
- default_config = st.session_state.get("default_config", {})
- dca_spreads = default_config.get("dca_spreads", [0.01, 0.02, 0.03])
- dca_amounts = default_config.get("dca_amounts", [0.2, 0.5, 0.3])
+ if controller_name:
+ default_config = get_controller_config(controller_name)
+ else:
+ # Fallback for backward compatibility
+ default_config = st.session_state.get("default_config", {})
+ dca_spreads = list(default_config.get("dca_spreads", [0.01, 0.02, 0.03]))
+ dca_amounts = list(default_config.get("dca_amounts", [0.2, 0.5, 0.3]))
tp = default_config.get("take_profit", 0.01) * 100
sl = default_config.get("stop_loss", 0.02) * 100
time_limit = default_config.get("time_limit", 60 * 6 * 60) // 60
diff --git a/frontend/components/deploy_v2_with_controllers.py b/frontend/components/deploy_v2_with_controllers.py
deleted file mode 100644
index 8de1e320..00000000
--- a/frontend/components/deploy_v2_with_controllers.py
+++ /dev/null
@@ -1,109 +0,0 @@
-import time
-
-import pandas as pd
-import streamlit as st
-
-from frontend.st_utils import get_backend_api_client
-
-
-class LaunchV2WithControllers:
- DEFAULT_COLUMNS = [
- "id", "controller_name", "controller_type", "connector_name",
- "trading_pair", "total_amount_quote", "max_loss_quote", "stop_loss",
- "take_profit", "trailing_stop", "time_limit", "selected"
- ]
-
- def __init__(self):
- 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 = "hummingbot/hummingbot:latest"
- self._credentials = "master_account"
-
- def _set_bot_name(self, bot_name):
- self._bot_name = bot_name
-
- def _set_image_name(self, image_name):
- self._image_name = image_name
-
- def _set_credentials(self, credentials):
- self._credentials = credentials
-
- def launch_new_bot(self):
- if self._bot_name and self._image_name and self._controller_config_selected:
- start_time_str = time.strftime("%Y.%m.%d_%H.%M")
- bot_name = f"{self._bot_name}-{start_time_str}"
- script_config = {
- "name": bot_name,
- "content": {
- "markets": {},
- "candles_config": [],
- "controllers_config": self._controller_config_selected,
- "script_file_name": "v2_with_controllers.py",
- }
- }
-
- self._backend_api_client.add_script_config(script_config)
- deploy_config = {
- "instance_name": bot_name,
- "script": "v2_with_controllers.py",
- "script_config": bot_name + ".yml",
- "image": self._image_name,
- "credentials_profile": self._credentials,
- }
- self._backend_api_client.create_hummingbot_instance(deploy_config)
- with st.spinner('Starting Bot... This process may take a few seconds'):
- time.sleep(3)
- else:
- st.warning("You need to define the bot name and select the controllers configs "
- "that you want to deploy.")
-
- def __call__(self):
- st.write("#### Select the controllers configs that you want to deploy.")
- all_controllers_config = self._controller_configs_available
- data = []
- for config in all_controllers_config:
- connector_name = config.get("connector_name", "Unknown")
- trading_pair = config.get("trading_pair", "Unknown")
- total_amount_quote = config.get("total_amount_quote", 0)
- stop_loss = config.get("stop_loss", 0)
- 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({
- "selected": False,
- "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": f"{stop_loss:.2%}",
- "take_profit": f"{take_profit:.2%}",
- "trailing_stop": f"{trailing_stop['activation_price']:.2%} / {trailing_stop['trailing_delta']:.2%}",
- "time_limit": time_limit,
- })
-
- df = pd.DataFrame(data)
-
- edited_df = st.data_editor(df, hide_index=True)
-
- self._controller_config_selected = [f"{config}.yml" for config in
- edited_df[edited_df["selected"]]["id"].tolist()]
- st.write(self._controller_config_selected)
- c1, c2, c3, c4 = st.columns([1, 1, 1, 0.3])
- with c1:
- self._bot_name = st.text_input("Instance Name")
- with c2:
- available_images = self._backend_api_client.get_available_images("hummingbot")
- self._image_name = st.selectbox("Hummingbot Image", available_images,
- index=available_images.index("hummingbot/hummingbot:latest"))
- with c3:
- available_credentials = self._backend_api_client.get_accounts()
- self._credentials = st.selectbox("Credentials", available_credentials, index=0)
- with c4:
- deploy_button = st.button("Deploy Bot")
- if deploy_button:
- self.launch_new_bot()
diff --git a/frontend/components/editor.py b/frontend/components/editor.py
deleted file mode 100644
index 7d3edfb8..00000000
--- a/frontend/components/editor.py
+++ /dev/null
@@ -1,92 +0,0 @@
-from functools import partial
-
-import streamlit as st
-from streamlit_elements import editor, event, lazy, mui, sync
-
-from backend.utils.os_utils import save_file
-
-from .dashboard import Dashboard
-
-
-class Editor(Dashboard.Item):
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- self._dark_theme = False
- self._index = 0
- self._tabs = {}
- self._editor_box_style = {
- "flex": 1,
- "minHeight": 0,
- "borderBottom": 1,
- "borderTop": 1,
- "borderColor": "divider"
- }
-
- def save_file(self):
- if len(self._tabs) > 0:
- label = list(self._tabs.keys())[self._index]
- content = self.get_content(label)
- file_name = label.split("/")[-1]
- path = "/".join(label.split("/")[:-1])
- save_file(name=file_name, content=content, path=path)
- st.info("File saved")
-
- def _change_tab(self, _, index):
- self._index = index
-
- @property
- def tabs(self):
- return self._tabs
-
- def update_content(self, label, content):
- self._tabs[label]["content"] = content
-
- def add_tab(self, label, default_content, language):
- self._tabs[label] = {
- "content": default_content,
- "language": language,
- }
-
- def remove_tab(self, label):
- del self._tabs[label]
-
- def get_content(self, label):
- return self._tabs[label]["content"]
-
- def __call__(self):
- with mui.Paper(key=self._key,
- sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"},
- elevation=1):
-
- with self.title_bar("0px 15px 0px 15px"):
- with mui.Grid(container=True, spacing=4, sx={"display": "flex", "alignItems": "center"}):
- with mui.Grid(item=True, xs=10, sx={"display": "flex", "alignItems": "center"}):
- mui.icon.Terminal()
- mui.Typography("Editor", variant="h6", sx={"marginLeft": 1})
- with mui.Tabs(value=self._index, onChange=self._change_tab, scrollButtons=True,
- variant="scrollable", sx={"flex": 1}):
- for label in self._tabs.keys():
- mui.Tab(label=label)
- with mui.Grid(item=True, xs=2, sx={"display": "flex", "justifyContent": "flex-end"}):
- mui.Button("Apply Changes", variant="contained", onClick=sync())
- mui.Button("Save Changes", variant="contained", onClick=self.save_file, sx={"mx": 1})
-
- for index, (label, tab) in enumerate(self._tabs.items()):
- with mui.Box(sx=self._editor_box_style, hidden=(index != self._index)):
- editor.Monaco(
- css={"padding": "0 2px 0 2px"},
- defaultValue=tab["content"],
- language=tab["language"],
- onChange=lazy(partial(self.update_content, label)),
- theme="vs-dark" if self._dark_mode else "light",
- path=label,
- options={
- "wordWrap": True,
- "fontSize": 16.5,
- }
- )
-
- with mui.Stack(direction="row", spacing=2, alignItems="center", sx={"padding": "10px"}):
- event.Hotkey("ctrl+s", sync(), bindInputs=True, overrideDefault=True)
diff --git a/frontend/components/executors_distribution.py b/frontend/components/executors_distribution.py
index 522f13e4..227bb602 100644
--- a/frontend/components/executors_distribution.py
+++ b/frontend/components/executors_distribution.py
@@ -10,11 +10,11 @@ def get_executors_distribution_inputs(use_custom_spread_units=False):
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_spreads = list(default_config.get("buy_spreads", [0.01, 0.02]))
+ sell_spreads = list(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_amounts_pct = default_config.get("buy_amounts_pct", default_amounts.copy())
+ sell_amounts_pct = default_config.get("sell_amounts_pct", default_amounts.copy())
buy_order_levels_def = len(buy_spreads)
sell_order_levels_def = len(sell_spreads)
with st.expander("Executors Configuration", expanded=True):
@@ -73,10 +73,9 @@ def get_executors_distribution_inputs(use_custom_spread_units=False):
sell_amount_scaling, sell_amount_step, sell_amount_ratio,
sell_manual_amounts)
- # Normalize and calculate order amounts
- all_orders_amount_normalized = normalize(buy_amount_distributions + sell_amount_distributions)
- buy_order_amounts_pct = [amount for amount in all_orders_amount_normalized[:buy_order_levels]]
- sell_order_amounts_pct = [amount for amount in all_orders_amount_normalized[buy_order_levels:]]
+ # Normalize buy and sell amounts separately to prevent cross-scaling
+ buy_order_amounts_pct = normalize(buy_amount_distributions)
+ sell_order_amounts_pct = normalize(sell_amount_distributions)
buy_spread_distributions = [spread / 100 for spread in buy_spread_distributions]
sell_spread_distributions = [spread / 100 for spread in sell_spread_distributions]
return buy_spread_distributions, sell_spread_distributions, buy_order_amounts_pct, sell_order_amounts_pct
diff --git a/frontend/components/file_explorer_base.py b/frontend/components/file_explorer_base.py
deleted file mode 100644
index 6481c4b7..00000000
--- a/frontend/components/file_explorer_base.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import streamlit as st
-from streamlit_elements import mui
-
-from backend.utils.os_utils import load_file, remove_file
-
-from .dashboard import Dashboard
-
-
-class FileExplorerBase(Dashboard.Item):
-
- def __init__(self, board, x, y, w, h, **item_props):
- super().__init__(board, x, y, w, h, **item_props)
- self._tabs = {}
- self.selected_file = None
-
- def set_selected_file(self, _, node_id):
- self.selected_file = node_id
-
- def delete_file(self):
- if self.is_file_editable:
- remove_file(self.selected_file)
- else:
- st.error("You can't delete the directory since it's a volume."
- "If you want to do it, go to the orchestrate tab and delete the container")
-
- @property
- def tabs(self):
- return self._tabs
-
- def add_file_to_tab(self):
- language = "python" if self.selected_file.endswith(".py") else "yaml"
- if self.is_file_editable:
- self._tabs[self.selected_file] = {
- "content": load_file(self.selected_file),
- "language": language}
-
- def remove_file_from_tab(self):
- if self.is_file_editable and self.selected_file in self._tabs:
- del self._tabs[self.selected_file]
-
- @property
- def is_file_editable(self):
- return self.selected_file and \
- (self.selected_file.endswith(".py") or self.selected_file.endswith(".yml")
- or "log" in self.selected_file)
-
- def add_tree_view(self):
- raise NotImplementedError
-
- 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):
- with mui.Grid(container=True, spacing=4, sx={"display": "flex", "alignItems": "center"}):
- with mui.Grid(item=True, xs=6, sx={"display": "flex", "alignItems": "center"}):
- mui.icon.Folder()
- mui.Typography("File Explorer", variant="h6", sx={"marginLeft": 1})
- with mui.Grid(item=True, xs=6, sx={"display": "flex", "justifyContent": "flex-end"}):
- mui.IconButton(mui.icon.Delete, onClick=self.delete_file, sx={"mx": 1})
- mui.IconButton(mui.icon.Edit, onClick=self.add_file_to_tab, sx={"mx": 1})
- mui.IconButton(mui.icon.Close, onClick=self.remove_file_from_tab, sx={"mx": 1})
- with mui.Box(sx={"overflow": "auto"}):
- self.add_tree_view()
diff --git a/frontend/components/launch_strategy_v2.py b/frontend/components/launch_strategy_v2.py
deleted file mode 100644
index 8dababff..00000000
--- a/frontend/components/launch_strategy_v2.py
+++ /dev/null
@@ -1,232 +0,0 @@
-import time
-
-import streamlit as st
-from streamlit_elements import lazy, mui
-
-from ..st_utils import get_backend_api_client
-from .dashboard import Dashboard
-
-
-class LaunchStrategyV2(Dashboard.Item):
- DEFAULT_ROWS = []
- DEFAULT_COLUMNS = [
- {"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, },
- {"field": 'trading_pair', "headerName": 'Trading pair', "width": 140, "editable": False, },
- {"field": 'total_amount_quote', "headerName": 'Total amount ($)', "width": 140, "editable": False, },
- {"field": 'max_loss_quote', "headerName": 'Max loss ($)', "width": 120, "editable": False, },
- {"field": 'stop_loss', "headerName": 'SL (%)', "width": 100, "editable": False, },
- {"field": 'take_profit', "headerName": 'TP (%)', "width": 100, "editable": False, },
- {"field": 'trailing_stop', "headerName": 'TS (%)', "width": 120, "editable": False, },
- {"field": 'time_limit', "headerName": 'Time limit', "width": 100, "editable": False, },
- ]
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- 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 = "hummingbot/hummingbot:latest"
- self._credentials = "master_account"
- self._max_global_drawdown = None
- self._max_controller_drawdown = None
- self._rebalance_interval = None
- self._asset_to_rebalance = "USDT"
-
- def _set_bot_name(self, event):
- self._bot_name = event.target.value
-
- def _set_image_name(self, _, childs):
- self._image_name = childs.props.value
-
- def _set_credentials(self, _, childs):
- self._credentials = childs.props.value
-
- def _set_controller(self, event):
- self._controller_selected = event.target.value
-
- def _handle_row_selection(self, params, _):
- self._controller_config_selected = [param + ".yml" for param in params]
-
- def _set_max_global_drawdown(self, event):
- self._max_global_drawdown = event.target.value
-
- def _set_max_controller_drawdown(self, event):
- self._max_controller_drawdown = event.target.value
-
- def _set_rebalance_interval(self, event):
- self._rebalance_interval = event.target.value
-
- def _set_asset_to_rebalance(self, event):
- self._asset_to_rebalance = event.target.value
-
- def launch_new_bot(self):
- if not self._bot_name:
- st.warning("You need to define the bot name.")
- return
- if not self._image_name:
- st.warning("You need to select the hummingbot image.")
- return
- if not self._controller_config_selected or len(self._controller_config_selected) == 0:
- st.warning("You need to select the controllers configs. Please select at least one controller "
- "config by clicking on the checkbox.")
- return
- start_time_str = time.strftime("%Y%m%d-%H%M")
- bot_name = f"{self._bot_name}-{start_time_str}"
- script_config = {
- "name": bot_name,
- "content": {
- "markets": {},
- "candles_config": [],
- "controllers_config": self._controller_config_selected,
- "script_file_name": "v2_with_controllers.py",
- "time_to_cash_out": None,
- }
- }
- if self._max_global_drawdown:
- script_config["content"]["max_global_drawdown"] = self._max_global_drawdown
- if self._max_controller_drawdown:
- script_config["content"]["max_controller_drawdown"] = self._max_controller_drawdown
- if self._rebalance_interval:
- script_config["content"]["rebalance_interval"] = self._rebalance_interval
- if self._asset_to_rebalance and "USD" in self._asset_to_rebalance:
- script_config["content"]["asset_to_rebalance"] = self._asset_to_rebalance
- else:
- st.error("You need to define the asset to rebalance in USD like token.")
- return
-
- self._backend_api_client.delete_all_script_configs()
- self._backend_api_client.add_script_config(script_config)
- deploy_config = {
- "instance_name": bot_name,
- "script": "v2_with_controllers.py",
- "script_config": bot_name + ".yml",
- "image": self._image_name,
- "credentials_profile": self._credentials,
- }
- self._backend_api_client.create_hummingbot_instance(deploy_config)
- with st.spinner('Starting Bot... This process may take a few seconds'):
- time.sleep(3)
-
- 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("🎛️ Bot Configuration", variant="h5")
-
- with mui.Grid(container=True, spacing=2, sx={"padding": "10px 15px 10px 15px"}):
- with mui.Grid(item=True, xs=3):
- mui.TextField(label="Instance Name", variant="outlined", onChange=lazy(self._set_bot_name),
- sx={"width": "100%"})
- with mui.Grid(item=True, xs=3):
- 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)
- available_credentials = self._backend_api_client.get_accounts()
- with mui.FormControl(variant="standard", sx={"width": "100%"}):
- mui.FormHelperText("Credentials")
- with mui.Select(label="Credentials", defaultValue="master_account",
- 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=3):
- with mui.FormControl(variant="standard", sx={"width": "100%"}):
- mui.FormHelperText("Risk Management")
- mui.TextField(label="Max Global Drawdown (%)", variant="outlined", type="number",
- onChange=lazy(self._set_max_global_drawdown), sx={"width": "100%"})
- mui.TextField(label="Max Controller Drawdown (%)", variant="outlined", type="number",
- onChange=lazy(self._set_max_controller_drawdown), sx={"width": "100%"})
-
- with mui.Grid(item=True, xs=3):
- with mui.FormControl(variant="standard", sx={"width": "100%"}):
- mui.FormHelperText("Rebalance Configuration")
- mui.TextField(label="Rebalance Interval (minutes)", variant="outlined", type="number",
- onChange=lazy(self._set_rebalance_interval), sx={"width": "100%"})
- mui.TextField(label="Asset to Rebalance", variant="outlined",
- onChange=lazy(self._set_asset_to_rebalance),
- sx={"width": "100%"}, default="USDT")
- all_controllers_config = self._backend_api_client.get_all_controllers_config()
- data = []
- for config in all_controllers_config:
- # Handle case where config might be a string instead of dict
- if isinstance(config, str):
- st.warning(f"Unexpected config format: {config}. Expected a dictionary.")
- continue
-
- connector_name = config.get("connector_name", "Unknown")
- trading_pair = config.get("trading_pair", "Unknown")
- total_amount_quote = float(config.get("total_amount_quote", 0))
- stop_loss = float(config.get("stop_loss", 0))
- take_profit = float(config.get("take_profit", 0))
- trailing_stop = config.get("trailing_stop", {"activation_price": 0, "trailing_delta": 0})
- time_limit = float(config.get("time_limit", 0))
- 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"
- ts_text = str(trailing_stop["activation_price"]) + " / " + str(trailing_stop["trailing_delta"])
- data.append({
- "id": config["id"], "config_base": config_base, "version": version,
- "controller_name": config["controller_name"],
- "controller_type": config.get("controller_type", "generic"),
- "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": ts_text,
- "time_limit": time_limit})
-
- with mui.Grid(item=True, xs=12):
- with mui.Paper(key=self._key,
- sx={"display": "flex", "flexDirection": "column", "borderRadius": 3,
- "overflow": "hidden", "height": 1000},
- elevation=2):
- with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False):
- 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,
- pageSize=15,
- rowsPerPageOptions=[15],
- checkboxSelection=True,
- disableSelectionOnClick=True,
- disableColumnResize=False,
- onSelectionModelChange=self._handle_row_selection,
- )
diff --git a/frontend/components/market_making_general_inputs.py b/frontend/components/market_making_general_inputs.py
index 0676ec7f..873436ba 100644
--- a/frontend/components/market_making_general_inputs.py
+++ b/frontend/components/market_making_general_inputs.py
@@ -1,10 +1,16 @@
import streamlit as st
+from frontend.components.config_loader import get_controller_config
-def get_market_making_general_inputs(custom_candles=False):
+
+def get_market_making_general_inputs(custom_candles=False, controller_name: str = None):
with st.expander("General Settings", expanded=True):
c1, c2, c3, c4, c5, c6, c7 = st.columns(7)
- default_config = st.session_state.get("default_config", {})
+ if controller_name:
+ default_config = get_controller_config(controller_name)
+ else:
+ # Fallback for backward compatibility
+ default_config = st.session_state.get("default_config", {})
connector_name = default_config.get("connector_name", "kucoin")
trading_pair = default_config.get("trading_pair", "WLD-USDT")
leverage = default_config.get("leverage", 20)
diff --git a/frontend/components/media_player.py b/frontend/components/media_player.py
deleted file mode 100644
index f816944b..00000000
--- a/frontend/components/media_player.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from streamlit_elements import lazy, media, mui, sync
-
-from .dashboard import Dashboard
-
-
-class Player(Dashboard.Item):
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self._url = "https://www.youtube.com/watch?v=CmSKVW1v0xM"
-
- def _set_url(self, event):
- self._url = event.target.value
-
- 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.icon.OndemandVideo()
- mui.Typography("Media player")
-
- with mui.Stack(direction="row", spacing=2, justifyContent="space-evenly", alignItems="center",
- sx={"padding": "10px"}):
- mui.TextField(defaultValue=self._url, label="URL", variant="standard", sx={"flex": 0.97},
- onChange=lazy(self._set_url))
- mui.IconButton(mui.icon.PlayCircleFilled, onClick=sync(), sx={"color": "primary.main"})
-
- media.Player(self._url, controls=True, width="100%", height="100%")
diff --git a/frontend/components/optimization_creation_card.py b/frontend/components/optimization_creation_card.py
deleted file mode 100644
index 3afbb1b6..00000000
--- a/frontend/components/optimization_creation_card.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import datetime
-
-from streamlit_elements import lazy, mui
-
-import constants
-from backend.utils.file_templates import strategy_optimization_template
-from backend.utils.os_utils import load_controllers, save_file
-
-from .dashboard import Dashboard
-
-
-class OptimizationCreationCard(Dashboard.Item):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- today = datetime.datetime.now()
- self._optimization_version = f"{today.day:02d}-{today.month:02d}-{today.year}"
- self._optimization_name = None
- self._strategy_name = None
-
- def _set_optimization_version(self, event):
- self._optimization_version = event.target.value
-
- def _set_strategy_name(self, _, childs):
- self._strategy_name = childs.props.value
-
- def _create_optimization(self, strategy_info):
- strategy_code = strategy_optimization_template(strategy_info)
- save_file(name=f"{self._strategy_name.lower()}_v_{self._optimization_version}.py", content=strategy_code,
- path=constants.OPTIMIZATIONS_PATH)
-
- def __call__(self):
- available_strategies = load_controllers(constants.CONTROLLERS_PATH)
- strategy_names = [strategy for strategy, strategy_info in available_strategies.items() if
- strategy_info["type"] == "directional_trading"]
- 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.icon.NoteAdd()
- mui.Typography("Create study", variant="h6")
- if len(strategy_names) == 0:
- mui.Alert("No strategies available, please create one to optimize it", severity="warning",
- sx={"width": "100%"})
- return
- else:
- if self._strategy_name is None:
- self._strategy_name = strategy_names[0]
- with mui.Grid(container=True, spacing=2, sx={"padding": "10px"}):
- with mui.Grid(item=True, xs=4):
- with mui.FormControl(variant="standard", sx={"width": "100%"}):
- mui.FormHelperText("Strategy name")
- with mui.Select(label="Select strategy", defaultValue=strategy_names[0],
- variant="standard", onChange=lazy(self._set_strategy_name)):
- for strategy in strategy_names:
- mui.MenuItem(strategy, value=strategy)
- with mui.Grid(item=True, xs=4):
- with mui.FormControl(variant="standard", sx={"width": "100%"}):
- mui.TextField(defaultValue=self._optimization_version, label="Optimization version",
- variant="standard", onChange=lazy(self._set_optimization_version))
- with mui.Grid(item=True, xs=4):
- with mui.Button(variant="contained", onClick=lambda x: self._create_optimization(
- available_strategies[self._strategy_name]), sx={"width": "100%"}):
- mui.icon.Add()
- mui.Typography("Create", variant="body1")
diff --git a/frontend/components/optimization_run_card.py b/frontend/components/optimization_run_card.py
deleted file mode 100644
index 822c64bb..00000000
--- a/frontend/components/optimization_run_card.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import threading
-
-import optuna
-from streamlit_elements import lazy, mui
-
-import constants
-from backend.utils.os_utils import get_function_from_file, get_python_files_from_directory
-
-from .dashboard import Dashboard
-
-
-class OptimizationRunCard(Dashboard.Item):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self._optimization_name = None
- self._number_of_trials = 2000
-
- def _set_optimization_name(self, _, childs):
- self._optimization_name = childs.props.value
-
- def _set_number_of_trials(self, event):
- self._number_of_trials = int(event.target.value)
-
- def _run_optimization(self):
- study_name = self._optimization_name.split('/')[-1].split('.')[0]
- study = optuna.create_study(direction="maximize", study_name=study_name,
- storage="sqlite:///data/backtesting/backtesting_report.db",
- load_if_exists=True)
- objective = get_function_from_file(file_path=self._optimization_name,
- function_name="objective")
-
- def optimization_process():
- study.optimize(objective, n_trials=self._number_of_trials)
-
- optimization_thread = threading.Thread(target=optimization_process)
- optimization_thread.start()
-
- def __call__(self):
- optimizations = get_python_files_from_directory(constants.OPTIMIZATIONS_PATH)
- 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.icon.AutoFixHigh()
- mui.Typography("Run a optimization", variant="h6")
-
- if len(optimizations) == 0:
- mui.Alert("No optimizations available, please create one.", severity="warning", sx={"width": "100%"})
- return
- else:
- if self._optimization_name is None:
- self._optimization_name = optimizations[0]
- with mui.Grid(container=True, spacing=2, sx={"padding": "10px"}):
- with mui.Grid(item=True, xs=4):
- with mui.FormControl(variant="standard", sx={"width": "100%"}):
- mui.FormHelperText("Study name")
- with mui.Select(defaultValue=optimizations[0],
- variant="standard", onChange=lazy(self._set_optimization_name)):
- for optimization in optimizations:
- mui.MenuItem(f"{optimization.split('/')[-1].split('.')[0]}", value=optimization)
- with mui.Grid(item=True, xs=4):
- with mui.FormControl(variant="standard", sx={"width": "100%"}):
- mui.TextField(defaultValue=self._optimization_name, label="Number of trials", type="number",
- variant="standard", onChange=lazy(self._set_number_of_trials))
- with mui.Grid(item=True, xs=4):
- with mui.Button(variant="contained", onClick=self._run_optimization, sx={"width": "100%"}):
- mui.icon.PlayCircleFilled()
- mui.Typography("Run", variant="button")
diff --git a/frontend/components/optimizations_file_explorer.py b/frontend/components/optimizations_file_explorer.py
deleted file mode 100644
index 901b93aa..00000000
--- a/frontend/components/optimizations_file_explorer.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from streamlit_elements import mui
-
-import constants
-from backend.utils.os_utils import get_python_files_from_directory
-from frontend.components.file_explorer_base import FileExplorerBase
-
-
-class OptimizationsStrategiesFileExplorer(FileExplorerBase):
- def add_tree_view(self):
- with mui.lab.TreeView(defaultExpandIcon=mui.icon.ChevronRight, defaultCollapseIcon=mui.icon.ExpandMore,
- onNodeSelect=lambda event, node_id: self.set_selected_file(event, node_id),
- defaultExpanded=["optimization_strategies"]):
- with mui.lab.TreeItem(nodeId="optimization_strategies", label=f"🔬Studies"):
- optimizations = get_python_files_from_directory(constants.OPTIMIZATIONS_PATH)
- for optimization in optimizations:
- mui.lab.TreeItem(nodeId=optimization, label=f"🐍{optimization.split('/')[-1]}")
diff --git a/frontend/components/save_config.py b/frontend/components/save_config.py
index 7b49b352..08cc37ff 100644
--- a/frontend/components/save_config.py
+++ b/frontend/components/save_config.py
@@ -1,18 +1,41 @@
import streamlit as st
+import nest_asyncio
from frontend.st_utils import get_backend_api_client
+nest_asyncio.apply()
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)
+ try:
+ all_configs = backend_api_client.controllers.list_controller_configs()
+ except Exception as e:
+ st.error(f"Failed to fetch controller configs: {e}")
+ return
+
+ # Handle both old and new config format
+ config_bases = set()
+ for config in all_configs:
+ config_name = config.get("id")
+ if config_name:
+ config_bases.add(config_name.split("_")[0])
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}"
+ config_tags = []
+ for config in all_configs:
+ config_name = config.get("id")
+ if config_name and config_base in config_name:
+ try:
+ config_tags.append(float(config_name.split("_")[-1]))
+ except (ValueError, IndexError):
+ continue
+ if config_tags:
+ config_tag = max(config_tags)
+ version, tag = str(config_tag).split(".")
+ config_tag = f"{version}.{int(tag) + 1}"
+ else:
+ config_tag = "0.1"
else:
config_tag = "0.1"
c1, c2, c3 = st.columns([1, 1, 0.5])
@@ -23,7 +46,14 @@ def render_save_config(config_base_default: str, config_data: dict):
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.add_controller_config(config_data)
- st.session_state.pop("default_config")
- st.success("Config uploaded successfully!")
+ config_name = f"{config_base}_{config_tag}"
+ config_data["id"] = config_name
+ try:
+ backend_api_client.controllers.create_or_update_controller_config(
+ config_name=config_name,
+ config=config_data
+ )
+ st.session_state.pop("default_config", None)
+ st.success("Config uploaded successfully!")
+ except Exception as e:
+ st.error(f"Failed to upload config: {e}")
diff --git a/frontend/pages/backtesting/__init__.py b/frontend/pages/backtesting/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/frontend/pages/backtesting/analyze/README.md b/frontend/pages/backtesting/analyze/README.md
deleted file mode 100644
index e11eabf3..00000000
--- a/frontend/pages/backtesting/analyze/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Deploy and manage backtests of directional strategies
\ No newline at end of file
diff --git a/frontend/pages/backtesting/analyze/__init__.py b/frontend/pages/backtesting/analyze/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/frontend/pages/backtesting/analyze/analyze.py b/frontend/pages/backtesting/analyze/analyze.py
deleted file mode 100644
index 4d7cda26..00000000
--- a/frontend/pages/backtesting/analyze/analyze.py
+++ /dev/null
@@ -1,244 +0,0 @@
-import json
-import os
-from decimal import Decimal
-
-import streamlit as st
-from hummingbot.core.data_type.common import OrderType, PositionMode, TradeType
-from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
-from hummingbot.strategy_v2.strategy_frameworks.data_types import OrderLevel, TripleBarrierConf
-from hummingbot.strategy_v2.strategy_frameworks.directional_trading import DirectionalTradingBacktestingEngine
-from hummingbot.strategy_v2.utils.config_encoder_decoder import ConfigEncoderDecoder
-
-import constants
-from backend.utils.optuna_database_manager import OptunaDBManager
-from backend.utils.os_utils import load_controllers
-from frontend.st_utils import initialize_st_page
-from frontend.visualization.graphs import BacktestingGraphs
-from frontend.visualization.strategy_analysis import StrategyAnalysis
-
-initialize_st_page(title="Analyze", icon="🔬")
-
-BASE_DATA_DIR = "data/backtesting"
-
-
-@st.cache_resource
-def get_databases():
- sqlite_files = [db_name for db_name in os.listdir(BASE_DATA_DIR) if db_name.endswith(".db")]
- databases_list = [OptunaDBManager(db, db_root_path=BASE_DATA_DIR) for db in sqlite_files]
- databases_dict = {database.db_name: database for database in databases_list}
- return [x.db_name for x in databases_dict.values() if x.status == 'OK']
-
-
-def initialize_session_state_vars():
- if "strategy_params" not in st.session_state:
- st.session_state.strategy_params = {}
- if "backtesting_params" not in st.session_state:
- st.session_state.backtesting_params = {}
-
-
-initialize_session_state_vars()
-dbs = get_databases()
-if not dbs:
- st.warning("We couldn't find any Optuna database.")
- selected_db_name = None
- selected_db = None
-else:
- # Select database from selectbox
- selected_db = st.selectbox("Select your database:", dbs)
- # Instantiate database manager
- opt_db = OptunaDBManager(selected_db, db_root_path=BASE_DATA_DIR)
- # Load studies
- studies = opt_db.load_studies()
- # Choose study
- study_selected = st.selectbox("Select a study:", studies.keys())
- # Filter trials from selected study
- merged_df = opt_db.merged_df[opt_db.merged_df["study_name"] == study_selected]
- filters_column, scatter_column = st.columns([1, 6])
- with filters_column:
- accuracy = st.slider("Accuracy", min_value=0.0, max_value=1.0, value=[0.4, 1.0], step=0.01)
- net_profit = st.slider("Net PNL (%)", min_value=merged_df["net_pnl_pct"].min(),
- max_value=merged_df["net_pnl_pct"].max(),
- value=[merged_df["net_pnl_pct"].min(), merged_df["net_pnl_pct"].max()], step=0.01)
- max_drawdown = st.slider("Max Drawdown (%)", min_value=merged_df["max_drawdown_pct"].min(),
- max_value=merged_df["max_drawdown_pct"].max(),
- value=[merged_df["max_drawdown_pct"].min(), merged_df["max_drawdown_pct"].max()],
- step=0.01)
- total_positions = st.slider("Total Positions", min_value=merged_df["total_positions"].min(),
- max_value=merged_df["total_positions"].max(),
- value=[merged_df["total_positions"].min(), merged_df["total_positions"].max()],
- step=1)
- net_profit_filter = (merged_df["net_pnl_pct"] >= net_profit[0]) & (merged_df["net_pnl_pct"] <= net_profit[1])
- accuracy_filter = (merged_df["accuracy"] >= accuracy[0]) & (merged_df["accuracy"] <= accuracy[1])
- max_drawdown_filter = (merged_df["max_drawdown_pct"] >= max_drawdown[0]) & (
- merged_df["max_drawdown_pct"] <= max_drawdown[1])
- total_positions_filter = (merged_df["total_positions"] >= total_positions[0]) & (
- merged_df["total_positions"] <= total_positions[1])
- with scatter_column:
- bt_graphs = BacktestingGraphs(
- merged_df[net_profit_filter & accuracy_filter & max_drawdown_filter & total_positions_filter])
- # Show and compare all of the study trials
- st.plotly_chart(bt_graphs.pnl_vs_maxdrawdown(), use_container_width=True)
- # Get study trials
- trials = studies[study_selected]
- # Choose trial
- trial_selected = st.selectbox("Select a trial to backtest", list(trials.keys()))
- trial = trials[trial_selected]
- # Transform trial config in a dictionary
- encoder_decoder = ConfigEncoderDecoder(TradeType, OrderType, PositionMode)
- trial_config = encoder_decoder.decode(json.loads(trial["config"]))
-
- # Strategy parameters section
- st.write("## Strategy parameters")
- # Load strategies (class, config, module)
- controllers = load_controllers(constants.CONTROLLERS_PATH)
- # Select strategy
- controller = controllers[trial_config["strategy_name"]]
- # Get field schema
- field_schema = controller["config"].schema()["properties"]
-
- columns = st.columns(4)
- column_index = 0
- for field_name, properties in field_schema.items():
- field_type = properties.get("type", "string")
- field_value = trial_config[field_name]
- if field_name not in ["candles_config", "order_levels", "position_mode"]:
- with columns[column_index]:
- if field_type in ["number", "integer"]:
- field_value = st.number_input(field_name,
- value=field_value,
- min_value=properties.get("minimum"),
- max_value=properties.get("maximum"),
- key=field_name)
- elif field_type == "string":
- field_value = st.text_input(field_name, value=field_value)
- elif field_type == "boolean":
- # TODO: Add support for boolean fields in optimize tab
- field_value = st.checkbox(field_name, value=field_value)
- else:
- raise ValueError("Field type {field_type} not supported")
- else:
- if field_name == "candles_config":
- st.write("---")
- st.write("## Candles Config:")
- candles = []
- for i, candles_config in enumerate(field_value):
- st.write(f"#### Candle {i}:")
- c11, c12, c13, c14 = st.columns(4)
- with c11:
- connector = st.text_input("Connector", value=candles_config["connector"])
- with c12:
- trading_pair = st.text_input("Trading pair", value=candles_config["trading_pair"])
- with c13:
- interval = st.text_input("Interval", value=candles_config["interval"])
- with c14:
- max_records = st.number_input("Max records", value=candles_config["max_records"])
- st.write("---")
- candles.append(CandlesConfig(connector=connector, trading_pair=trading_pair, interval=interval,
- max_records=max_records))
- field_value = candles
- elif field_name == "order_levels":
- new_levels = []
- st.write("## Order Levels:")
- for order_level in field_value:
- st.write(f"### Level {order_level['level']} {order_level['side'].name}")
- ol_c1, ol_c2 = st.columns([5, 1])
- with ol_c1:
- st.write("#### Triple Barrier config:")
- c21, c22, c23, c24, c25 = st.columns(5)
- triple_barrier_conf_level = order_level["triple_barrier_conf"]
- with c21:
- take_profit = st.number_input("Take profit",
- value=float(triple_barrier_conf_level["take_profit"]),
- key=f"{order_level['level']}_{order_level['side'].name}_tp")
- with c22:
- stop_loss = st.number_input("Stop Loss",
- value=float(triple_barrier_conf_level["stop_loss"]),
- key=f"{order_level['level']}_{order_level['side'].name}_sl")
- with c23:
- time_limit = st.number_input("Time Limit", value=triple_barrier_conf_level["time_limit"],
- key=f"{order_level['level']}_{order_level['side'].name}_tl")
- with c24:
- ts_ap = st.number_input("Trailing Stop Activation Price", value=float(
- triple_barrier_conf_level["trailing_stop_activation_price_delta"]),
- key=f"{order_level['level']}_{order_level['side'].name}_tsap",
- format="%.4f")
- with c25:
- ts_td = st.number_input("Trailing Stop Trailing Delta", value=float(
- triple_barrier_conf_level["trailing_stop_trailing_delta"]),
- key=f"{order_level['level']}_{order_level['side'].name}_tstd",
- format="%.4f")
- with ol_c2:
- st.write("#### Position config:")
- c31, c32 = st.columns(2)
- with c31:
- order_amount = st.number_input("Order amount USD",
- value=float(order_level["order_amount_usd"]),
- key=f"{order_level['level']}_{order_level['side'].name}_oa")
- with c32:
- cooldown_time = st.number_input("Cooldown time", value=order_level["cooldown_time"],
- key=f"{order_level['level']}_{order_level['side'].name}_cd")
- triple_barrier_conf = TripleBarrierConf(stop_loss=Decimal(stop_loss),
- take_profit=Decimal(take_profit),
- time_limit=time_limit,
- trailing_stop_activation_price_delta=Decimal(ts_ap),
- trailing_stop_trailing_delta=Decimal(ts_td),
- open_order_type=OrderType.MARKET)
- new_levels.append(OrderLevel(level=order_level["level"], side=order_level["side"],
- order_amount_usd=order_amount, cooldown_time=cooldown_time,
- triple_barrier_conf=triple_barrier_conf))
- st.write("---")
-
- field_value = new_levels
- elif field_name == "position_mode":
- field_value = PositionMode.HEDGE
- else:
- field_value = None
- st.session_state["strategy_params"][field_name] = field_value
-
- column_index = (column_index + 1) % 4
-
- st.write("### Backtesting period")
- col1, col2, col3, col4 = st.columns([1, 1, 1, 0.5])
- with col1:
- trade_cost = st.number_input("Trade cost",
- value=0.0006,
- min_value=0.0001, format="%.4f", )
- with col2:
- initial_portfolio_usd = st.number_input("Initial portfolio usd",
- value=10000.00,
- min_value=1.00,
- max_value=999999999.99)
- with col3:
- start = st.text_input("Start", value="2023-01-01")
- end = st.text_input("End", value="2024-01-01")
- c1, c2 = st.columns([1, 1])
- with col4:
- add_positions = st.checkbox("Add positions", value=True)
- add_volume = st.checkbox("Add volume", value=True)
- add_pnl = st.checkbox("Add PnL", value=True)
- save_config = st.button("💾Save controller config!")
- config = controller["config"](**st.session_state["strategy_params"])
- controller = controller["class"](config=config)
- if save_config:
- encoder_decoder = ConfigEncoderDecoder(TradeType, OrderType, PositionMode)
- encoder_decoder.yaml_dump(config.dict(),
- f"hummingbot_files/controller_configs/{config.strategy_name}_{trial_selected}.yml")
- run_backtesting_button = st.button("⚙️Run Backtesting!")
- if run_backtesting_button:
- try:
- engine = DirectionalTradingBacktestingEngine(controller=controller)
- engine.load_controller_data("./data/candles")
- backtesting_results = engine.run_backtesting(initial_portfolio_usd=initial_portfolio_usd,
- trade_cost=trade_cost,
- start=start, end=end)
- strategy_analysis = StrategyAnalysis(
- positions=backtesting_results["executors_df"],
- candles_df=backtesting_results["processed_data"],
- )
- metrics_container = BacktestingGraphs(backtesting_results["processed_data"]).get_trial_metrics(
- strategy_analysis,
- add_positions=add_positions,
- add_volume=add_volume)
-
- except FileNotFoundError:
- st.warning("The requested candles could not be found.")
diff --git a/frontend/pages/backtesting/create/README.md b/frontend/pages/backtesting/create/README.md
deleted file mode 100644
index e11eabf3..00000000
--- a/frontend/pages/backtesting/create/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Deploy and manage backtests of directional strategies
\ No newline at end of file
diff --git a/frontend/pages/backtesting/create/__init__.py b/frontend/pages/backtesting/create/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/frontend/pages/backtesting/create/create.py b/frontend/pages/backtesting/create/create.py
deleted file mode 100644
index bdea7fc7..00000000
--- a/frontend/pages/backtesting/create/create.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from types import SimpleNamespace
-
-import streamlit as st
-from streamlit_elements import elements, mui
-
-from frontend.components.controllers_file_explorer import ControllersFileExplorer
-from frontend.components.dashboard import Dashboard
-from frontend.components.directional_strategy_creation_card import DirectionalStrategyCreationCard
-from frontend.components.editor import Editor
-from frontend.st_utils import initialize_st_page
-
-initialize_st_page(title="Create", icon="️⚔️")
-
-# TODO:
-# * Add videos explaining how to the triple barrier method works and how the backtesting is designed,
-# link to video of how to create a strategy, etc in a toggle.
-# * Add functionality to start strategy creation from scratch or by duplicating an existing one
-
-if "ds_board" not in st.session_state:
- board = Dashboard()
- ds_board = SimpleNamespace(
- dashboard=board,
- create_strategy_card=DirectionalStrategyCreationCard(board, 0, 0, 12, 1),
- file_explorer=ControllersFileExplorer(board, 0, 2, 3, 7),
- editor=Editor(board, 4, 2, 9, 7),
- )
- st.session_state.ds_board = ds_board
-
-else:
- ds_board = st.session_state.ds_board
-
-# Add new tabs
-for tab_name, content in ds_board.file_explorer.tabs.items():
- if tab_name not in ds_board.editor.tabs:
- ds_board.editor.add_tab(tab_name, content["content"], content["language"])
-
-# Remove deleted tabs
-for tab_name in list(ds_board.editor.tabs.keys()):
- if tab_name not in ds_board.file_explorer.tabs:
- ds_board.editor.remove_tab(tab_name)
-
-with elements("directional_strategies"):
- with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True):
- with ds_board.dashboard():
- ds_board.create_strategy_card()
- ds_board.file_explorer()
- ds_board.editor()
diff --git a/frontend/pages/backtesting/optimize/README.md b/frontend/pages/backtesting/optimize/README.md
deleted file mode 100644
index e11eabf3..00000000
--- a/frontend/pages/backtesting/optimize/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Deploy and manage backtests of directional strategies
\ No newline at end of file
diff --git a/frontend/pages/backtesting/optimize/__init__.py b/frontend/pages/backtesting/optimize/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/frontend/pages/backtesting/optimize/optimize.py b/frontend/pages/backtesting/optimize/optimize.py
deleted file mode 100644
index 673df01d..00000000
--- a/frontend/pages/backtesting/optimize/optimize.py
+++ /dev/null
@@ -1,62 +0,0 @@
-import time
-import webbrowser
-from types import SimpleNamespace
-
-import streamlit as st
-from streamlit_elements import elements, mui
-
-from backend.utils import os_utils
-from frontend.components.dashboard import Dashboard
-from frontend.components.editor import Editor
-from frontend.components.optimization_creation_card import OptimizationCreationCard
-from frontend.components.optimization_run_card import OptimizationRunCard
-from frontend.components.optimizations_file_explorer import OptimizationsStrategiesFileExplorer
-from frontend.st_utils import initialize_st_page
-
-initialize_st_page(title="Optimize", icon="🧪")
-
-
-def run_optuna_dashboard():
- os_utils.execute_bash_command("optuna-dashboard sqlite:///data/backtesting/backtesting_report.db")
- time.sleep(5)
- webbrowser.open("http://127.0.0.1:8080/dashboard", new=2)
-
-
-if "op_board" not in st.session_state:
- board = Dashboard()
- op_board = SimpleNamespace(
- dashboard=board,
- create_optimization_card=OptimizationCreationCard(board, 0, 0, 6, 1),
- run_optimization_card=OptimizationRunCard(board, 6, 0, 6, 1),
- file_explorer=OptimizationsStrategiesFileExplorer(board, 0, 2, 3, 7),
- editor=Editor(board, 4, 2, 9, 7),
- )
- st.session_state.op_board = op_board
-
-else:
- op_board = st.session_state.op_board
-
-# Add new tabs
-for tab_name, content in op_board.file_explorer.tabs.items():
- if tab_name not in op_board.editor.tabs:
- op_board.editor.add_tab(tab_name, content["content"], content["language"])
-
-# Remove deleted tabs
-for tab_name in list(op_board.editor.tabs.keys()):
- if tab_name not in op_board.file_explorer.tabs:
- op_board.editor.remove_tab(tab_name)
-
-with elements("optimizations"):
- with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True):
- with mui.Grid(container=True, spacing=2):
- with mui.Grid(item=True, xs=10):
- pass
- with mui.Grid(item=True, xs=2):
- with mui.Fab(variant="extended", color="primary", size="large", onClick=run_optuna_dashboard):
- mui.Typography("Open Optuna Dashboard", variant="body1")
-
- with op_board.dashboard():
- op_board.create_optimization_card()
- op_board.run_optimization_card()
- op_board.file_explorer()
- op_board.editor()
diff --git a/frontend/pages/config/kalman_filter_v1/app.py b/frontend/pages/config/kalman_filter_v1/app.py
index cee2f4f3..96d69fb0 100644
--- a/frontend/pages/config/kalman_filter_v1/app.py
+++ b/frontend/pages/config/kalman_filter_v1/app.py
@@ -225,5 +225,12 @@ def add_indicators(df, observation_covariance=1, transition_covariance=0.01, ini
if upload_config_to_backend:
backend_api_client = get_backend_api_client()
- backend_api_client.add_controller_config(config)
- st.success("Config uploaded successfully!")
+ try:
+ config_name = config.get("id", id)
+ backend_api_client.controllers.create_or_update_controller_config(
+ config_name=config_name,
+ config=config
+ )
+ st.success("Config uploaded successfully!")
+ except Exception as e:
+ st.error(f"Failed to upload config: {e}")
diff --git a/frontend/pages/config/utils.py b/frontend/pages/config/utils.py
index 8d90252a..71e3dfb2 100644
--- a/frontend/pages/config/utils.py
+++ b/frontend/pages/config/utils.py
@@ -16,11 +16,17 @@ def get_max_records(days_to_download: int, interval: str) -> int:
@st.cache_data
def get_candles(connector_name="binance", trading_pair="BTC-USDT", interval="1m", days=7):
backend_client = get_backend_api_client()
- end_time = datetime.datetime.now() - datetime.timedelta(minutes=15)
- start_time = end_time - datetime.timedelta(days=days)
-
- df = pd.DataFrame(backend_client.get_historical_candles(connector_name, trading_pair, interval,
- start_time=int(start_time.timestamp()),
- end_time=int(end_time.timestamp())))
- df.index = pd.to_datetime(df.timestamp, unit='s')
+
+ # Use the market_data.get_candles_last_days method
+ candles = backend_client.market_data.get_candles_last_days(
+ connector_name=connector_name,
+ trading_pair=trading_pair,
+ days=days,
+ interval=interval
+ )
+
+ # Convert the response to DataFrame (response is a list of candles)
+ df = pd.DataFrame(candles)
+ if not df.empty and 'timestamp' in df.columns:
+ df.index = pd.to_datetime(df.timestamp, unit='s')
return df
diff --git a/frontend/pages/config/xemm_controller/app.py b/frontend/pages/config/xemm_controller/app.py
index f916dadf..6767b1d7 100644
--- a/frontend/pages/config/xemm_controller/app.py
+++ b/frontend/pages/config/xemm_controller/app.py
@@ -130,5 +130,12 @@ def create_order_graph(order_type, targets, min_profit, max_profit):
if upload_config_to_backend:
backend_api_client = get_backend_api_client()
- backend_api_client.add_controller_config(config)
- st.success("Config uploaded successfully!")
+ try:
+ config_name = config.get("id", id.lower())
+ backend_api_client.controllers.create_or_update_controller_config(
+ config_name=config_name,
+ config=config
+ )
+ st.success("Config uploaded successfully!")
+ except Exception as e:
+ st.error(f"Failed to upload config: {e}")
diff --git a/frontend/pages/data/download_candles/app.py b/frontend/pages/data/download_candles/app.py
index e6050842..c4c7da4c 100644
--- a/frontend/pages/data/download_candles/app.py
+++ b/frontend/pages/data/download_candles/app.py
@@ -31,8 +31,8 @@
st.error("End Date should be greater than Start Date.")
st.stop()
- candles = backend_api_client.get_historical_candles(
- connector=connector,
+ candles = backend_api_client.market_data.get_historical_candles(
+ connector_name=connector,
trading_pair=trading_pair,
interval=interval,
start_time=int(start_datetime.timestamp()),
@@ -48,9 +48,7 @@
open=candles_df['open'],
high=candles_df['high'],
low=candles_df['low'],
- close=candles_df['close'],
- increasing_line_color='#2ECC71',
- decreasing_line_color='#E74C3C'
+ close=candles_df['close']
)])
fig.update_layout(
height=1000,
diff --git a/frontend/pages/data/token_spreads/README.md b/frontend/pages/data/token_spreads/README.md
deleted file mode 100644
index 4225df86..00000000
--- a/frontend/pages/data/token_spreads/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Identify cross-exchange trading opportunities by analyzing differences in token spreads across venues
\ No newline at end of file
diff --git a/frontend/pages/data/token_spreads/__init__.py b/frontend/pages/data/token_spreads/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/frontend/pages/data/token_spreads/app.py b/frontend/pages/data/token_spreads/app.py
deleted file mode 100644
index 5581f68d..00000000
--- a/frontend/pages/data/token_spreads/app.py
+++ /dev/null
@@ -1,84 +0,0 @@
-import plotly.express as px
-import streamlit as st
-
-import CONFIG
-from backend.services.coingecko_client import CoinGeckoClient
-from backend.services.miner_client import MinerClient
-from frontend.st_utils import initialize_st_page
-
-initialize_st_page(title="Token Spreads", icon="🧙")
-
-# Start content here
-cg_utils = CoinGeckoClient()
-miner_utils = MinerClient()
-
-
-@st.cache_data
-def get_all_coins_df():
- return cg_utils.get_all_coins_df()
-
-
-@st.cache_data
-def get_all_exchanges_df():
- return cg_utils.get_all_exchanges_df()
-
-
-@st.cache_data
-def get_miner_stats_df():
- return miner_utils.get_miner_stats_df()
-
-
-@st.cache_data
-def get_coin_tickers_by_id_list(coins_id: list):
- return cg_utils.get_coin_tickers_by_id_list(coins_id)
-
-
-with st.spinner(text='In progress'):
- exchanges_df = get_all_exchanges_df()
- coins_df = get_all_coins_df()
- miner_stats_df = get_miner_stats_df()
-
-miner_coins = coins_df.loc[coins_df["symbol"].isin(miner_stats_df["base"].str.lower().unique()), "name"]
-
-tokens = st.multiselect(
- "Select the tokens to analyze:",
- options=coins_df["name"],
- default=CONFIG.DEFAULT_MINER_COINS
-)
-
-coins_id = coins_df.loc[coins_df["name"].isin(tokens), "id"].tolist()
-
-coin_tickers_df = get_coin_tickers_by_id_list(coins_id)
-coin_tickers_df["coin_name"] = coin_tickers_df.apply(
- lambda x: coins_df.loc[coins_df["id"] == x.token_id, "name"].item(), axis=1)
-
-exchanges = st.multiselect(
- "Select the exchanges to analyze:",
- options=exchanges_df["name"],
- default=[exchange for exchange in CONFIG.MINER_EXCHANGES if exchange in exchanges_df["name"].unique()]
-)
-
-height = len(coin_tickers_df["coin_name"].unique()) * 500
-
-fig = px.scatter(
- data_frame=coin_tickers_df[coin_tickers_df["exchange"].isin(exchanges)],
- x="volume",
- y="bid_ask_spread_percentage",
- color="exchange",
- log_x=True,
- log_y=True,
- facet_col="coin_name",
- hover_data=["trading_pair"],
- facet_col_wrap=1,
- height=height,
- template="plotly_dark",
- title="Spread and Volume Chart",
- labels={
- "volume": 'Volume (USD)',
- 'bid_ask_spread_percentage': 'Bid Ask Spread (%)'
- }
-)
-
-# st.write("# Data filters 🏷")
-# st.code("🧳 New filters coming. \nReach us on discord \nif you want to propose one!")
-st.plotly_chart(fig, use_container_width=True)
diff --git a/frontend/pages/landing.py b/frontend/pages/landing.py
index e63fa3a5..fc56f29b 100644
--- a/frontend/pages/landing.py
+++ b/frontend/pages/landing.py
@@ -1,17 +1,325 @@
+import random
+from datetime import datetime, timedelta
+
+import pandas as pd
+import plotly.graph_objects as go
import streamlit as st
-# readme section
-st.markdown("# 📊 Hummingbot Dashboard")
-st.markdown("Hummingbot Dashboard is an open source application that helps you create, backtest, and optimize "
- "various types of algo trading strategies. Afterwards, you can deploy them as "
- "[Hummingbot](http://hummingbot.org)")
-st.write("---")
-st.header("Watch the Hummingbot Dashboard Tutorial!")
-st.video("https://youtu.be/7eHiMPRBQLQ?si=PAvCq0D5QDZz1h1D")
-st.header("Feedback and issues")
-st.write(
- "Please give us feedback in the **#dashboard** channel of the "
- "[hummingbot discord](https://discord.gg/hummingbot)! 🙏")
-st.write(
- "If you encounter any bugs or have suggestions for improvement, please create an issue in the "
- "[hummingbot dashboard github](https://github.com/hummingbot/dashboard).")
+from frontend.st_utils import initialize_st_page
+
+initialize_st_page(
+ layout="wide",
+ show_readme=False
+)
+
+# Custom CSS for enhanced styling
+st.markdown("""
+
+""", unsafe_allow_html=True)
+
+# Hero Section
+st.markdown("""
+
+ Your Command Center for Algorithmic Trading Excellence +
+Currently Trading
++2.3% Today
+Last 30 Days
+This Month
+Join thousands of algorithmic traders sharing strategies and insights!
+