diff --git a/.streamlit/config.toml b/.streamlit/config.toml
index 9a427db..f6e4364 100644
--- a/.streamlit/config.toml
+++ b/.streamlit/config.toml
@@ -1,5 +1,5 @@
[theme]
-base="light"
+base="dark"
font="monospace"
[server]
port=8501
diff --git a/CONFIG.py b/CONFIG.py
index 4cf953f..527f29b 100644
--- a/CONFIG.py
+++ b/CONFIG.py
@@ -1,3 +1,10 @@
+import os
+
+from dotenv import load_dotenv
+
+
+load_dotenv()
+
MINER_COINS = ["Algorand", "Avalanche", "DAO Maker", "Faith Tribe", "Fear", "Frontier",
"Harmony", "Hot Cross", "HUMAN Protocol", "Oddz", "Shera", "Firo",
"Vesper Finance", "Youclout", "Nimiq"]
@@ -12,3 +19,6 @@ CERTIFIED_EXCHANGES = ["ascendex", "binance", "bybit", "gate.io", "hitbtc", "huo
CERTIFIED_STRATEGIES = ["xemm", "cross exchange market making", "pmm", "pure market making"]
AUTH_SYSTEM_ENABLED = False
+
+BACKEND_API_HOST = os.getenv("BACKEND_API_HOST", "127.0.0.1")
+BACKEND_API_PORT = os.getenv("BACKEND_API_PORT", 8000)
diff --git a/Dockerfile b/Dockerfile
index 865eb80..d117555 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -39,18 +39,11 @@ ENV COMMIT_SHA=${COMMIT}
ENV COMMIT_BRANCH=${BRANCH}
ENV BUILD_DATE=${DATE}
-ENV INSTALLATION_TYPE=docker
-
# Install system dependencies
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
-# Install Docker CLI
-RUN curl -fsSL https://get.docker.com -o get-docker.sh && \
- sh get-docker.sh && \
- rm get-docker.sh
-
# Create mount points
RUN mkdir -p /home/dashboard/data
diff --git a/Makefile b/Makefile
index 14be7c3..830c46c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,19 +1,19 @@
.ONESHELL:
.PHONY: run
-.PHONY: conda_remove
-.PHONY: conda_create
+.PHONY: uninstall
+.PHONY: install
run:
streamlit run main.py
-env_remove:
+uninstall:
conda env remove -n dashboard
-env_create:
+install:
conda env create -f environment_conda.yml
docker_build:
- docker build -t dashboard:latest .
+ docker build -t hummingbot/dashboard:latest .
docker_run:
docker run -p 8501:8501 dashboard:latest
\ No newline at end of file
diff --git a/quants_lab/controllers/__init__.py b/backend/__init__.py
similarity index 100%
rename from quants_lab/controllers/__init__.py
rename to backend/__init__.py
diff --git a/quants_lab/labeling/__init__.py b/backend/services/__init__.py
similarity index 100%
rename from quants_lab/labeling/__init__.py
rename to backend/services/__init__.py
diff --git a/backend/services/backend_api_client.py b/backend/services/backend_api_client.py
new file mode 100644
index 0000000..786eacf
--- /dev/null
+++ b/backend/services/backend_api_client.py
@@ -0,0 +1,281 @@
+import pandas as pd
+import requests
+from hummingbot.strategy_v2.models.executors_info import ExecutorInfo
+
+
+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) -> "MarketsRecorder":
+ 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):
+ self.host = host
+ self.port = port
+ self.base_url = f"http://{self.host}:{self.port}"
+
+ def is_docker_running(self):
+ """Check if Docker is running."""
+ url = f"{self.base_url}/is-docker-running"
+ response = requests.get(url)
+ return response.json()
+
+ def pull_image(self, image_name: str):
+ """Pull a Docker image."""
+ url = f"{self.base_url}/pull-image/"
+ payload = {"image_name": image_name}
+ response = requests.post(url, json=payload)
+ return response.json()
+
+ def list_available_images(self, image_name: str):
+ """List available images by name."""
+ url = f"{self.base_url}/available-images/{image_name}"
+ response = requests.get(url)
+ return response.json()
+
+ def list_active_containers(self):
+ """List all active containers."""
+ url = f"{self.base_url}/active-containers"
+ response = requests.get(url)
+ return response.json()
+
+ def list_exited_containers(self):
+ """List all exited containers."""
+ url = f"{self.base_url}/exited-containers"
+ response = requests.get(url)
+ return response.json()
+
+ def clean_exited_containers(self):
+ """Clean up exited containers."""
+ url = f"{self.base_url}/clean-exited-containers"
+ response = requests.post(url)
+ return response.json()
+
+ def remove_container(self, container_name: str, archive_locally: bool = True, s3_bucket: str = None):
+ """Remove a specific container."""
+ url = f"{self.base_url}/remove-container/{container_name}"
+ params = {"archive_locally": archive_locally}
+ if s3_bucket:
+ params["s3_bucket"] = s3_bucket
+ response = requests.post(url, params=params)
+ return response.json()
+
+ def stop_container(self, container_name: str):
+ """Stop a specific container."""
+ url = f"{self.base_url}/stop-container/{container_name}"
+ response = requests.post(url)
+ return response.json()
+
+ def start_container(self, container_name: str):
+ """Start a specific container."""
+ url = f"{self.base_url}/start-container/{container_name}"
+ response = requests.post(url)
+ return response.json()
+
+ def create_hummingbot_instance(self, instance_config: dict):
+ """Create a new Hummingbot instance."""
+ url = f"{self.base_url}/create-hummingbot-instance"
+ response = requests.post(url, json=instance_config)
+ return response.json()
+
+ def start_bot(self, start_bot_config: dict):
+ """Start a Hummingbot bot."""
+ url = f"{self.base_url}/start-bot"
+ response = requests.post(url, json=start_bot_config)
+ return response.json()
+
+ def stop_bot(self, bot_name: str, skip_order_cancellation: bool = False, async_backend: bool = True):
+ """Stop a Hummingbot bot."""
+ url = f"{self.base_url}/stop-bot"
+ response = requests.post(url, json={"bot_name": bot_name, "skip_order_cancellation": skip_order_cancellation, "async_backend": async_backend})
+ return response.json()
+
+ def import_strategy(self, strategy_config: dict):
+ """Import a trading strategy to a bot."""
+ url = f"{self.base_url}/import-strategy"
+ response = requests.post(url, json=strategy_config)
+ return response.json()
+
+ def get_bot_status(self, bot_name: str):
+ """Get the status of a bot."""
+ url = f"{self.base_url}/get-bot-status/{bot_name}"
+ response = requests.get(url)
+ if response.status_code == 200:
+ return response.json()
+ else:
+ return {"status": "error", "data": "Bot not found"}
+
+ def get_bot_history(self, bot_name: str):
+ """Get the historical data of a bot."""
+ url = f"{self.base_url}/get-bot-history/{bot_name}"
+ response = requests.get(url)
+ return response.json()
+
+ def get_active_bots_status(self):
+ """
+ Retrieve the cached status of all active bots.
+ Returns a JSON response with the status and data of active bots.
+ """
+ url = f"{self.base_url}/get-active-bots-status"
+ response = requests.get(url)
+ if response.status_code == 200:
+ return response.json() # Successful request
+ else:
+ return {"status": "error", "data": "No active bots found"}
+
+ def get_all_controllers_config(self):
+ """Get all controller configurations."""
+ url = f"{self.base_url}/all-controller-configs"
+ response = requests.get(url)
+ return response.json()
+
+ def get_available_images(self, image_name: str = "hummingbot"):
+ """Get available images."""
+ url = f"{self.base_url}/available-images/{image_name}"
+ response = requests.get(url)
+ return response.json()["available_images"]
+
+ def add_script_config(self, script_config: dict):
+ """Add a new script configuration."""
+ url = f"{self.base_url}/add-script-config"
+ response = requests.post(url, json=script_config)
+ return response.json()
+
+ def add_controller_config(self, controller_config: dict):
+ """Add a new controller configuration."""
+ url = f"{self.base_url}/add-controller-config"
+ config = {
+ "name": controller_config["id"],
+ "content": controller_config
+ }
+ response = requests.post(url, json=config)
+ return response.json()
+
+ def get_real_time_candles(self, connector: str, trading_pair: str, interval: str, max_records: int):
+ """Get candles data."""
+ url = f"{self.base_url}/real-time-candles"
+ payload = {
+ "connector": connector,
+ "trading_pair": trading_pair,
+ "interval": interval,
+ "max_records": max_records
+ }
+ response = requests.post(url, json=payload)
+ return response.json()
+
+ def get_historical_candles(self, connector: str, trading_pair: str, interval: str, start_time: int, end_time: int):
+ """Get historical candles data."""
+ url = f"{self.base_url}/historical-candles"
+ payload = {
+ "connector": connector,
+ "trading_pair": trading_pair,
+ "interval": interval,
+ "start_time": start_time,
+ "end_time": end_time
+ }
+ response = requests.post(url, json=payload)
+ return response.json()
+
+ def run_backtesting(self, start_time: int, end_time: int, backtesting_resolution: str, trade_cost: float, config: dict):
+ """Run backtesting."""
+ url = f"{self.base_url}/run-backtesting"
+ payload = {
+ "start_time": start_time,
+ "end_time": end_time,
+ "backtesting_resolution": backtesting_resolution,
+ "trade_cost": trade_cost,
+ "config": config
+ }
+ response = requests.post(url, json=payload)
+ backtesting_results = response.json()
+ if "processed_data" not in backtesting_results:
+ data = None
+ else:
+ data = pd.DataFrame(backtesting_results["processed_data"])
+ return {
+ "processed_data": data,
+ "executors": [ExecutorInfo(**executor) for executor in backtesting_results["executors"]],
+ "results": backtesting_results["results"]
+ }
+
+ def get_all_configs_from_bot(self, bot_name: str):
+ """Get all configurations from a bot."""
+ url = f"{self.base_url}/all-controller-configs/bot/{bot_name}"
+ response = requests.get(url)
+ return response.json()
+
+ def stop_controller_from_bot(self, bot_name: str, controller_id: str):
+ """Stop a controller from a bot."""
+ config = {"manual_kill_switch": True}
+ url = f"{self.base_url}/update-controller-config/bot/{bot_name}/{controller_id}"
+ response = requests.post(url, json=config)
+ return response.json()
+
+ def start_controller_from_bot(self, bot_name: str, controller_id: str):
+ """Start a controller from a bot."""
+ config = {"manual_kill_switch": False}
+ url = f"{self.base_url}/update-controller-config/bot/{bot_name}/{controller_id}"
+ response = requests.post(url, json=config)
+ return response.json()
+
+ def get_connector_config_map(self, connector_name: str):
+ """Get connector configuration map."""
+ url = f"{self.base_url}/connector-config-map/{connector_name}"
+ response = requests.get(url)
+ return response.json()
+
+ def get_all_connectors_config_map(self):
+ """Get all connector configuration maps."""
+ url = f"{self.base_url}/all-connectors-config-map"
+ response = requests.get(url)
+ return response.json()
+
+ def add_account(self, account_name: str):
+ """Add a new account."""
+ url = f"{self.base_url}/add-account"
+ response = requests.post(url, params={"account_name": account_name})
+ return response.json()
+
+ def delete_account(self, account_name: str):
+ """Delete an account."""
+ url = f"{self.base_url}/delete-account/"
+ response = requests.post(url, params={"account_name": account_name})
+ return response.json()
+
+ def delete_credential(self, account_name: str, connector_name: str):
+ """Delete credentials."""
+ url = f"{self.base_url}/delete-credential/{account_name}/{connector_name}"
+ response = requests.post(url)
+ return response.json()
+
+ def add_connector_keys(self, account_name: str, connector_name: str, connector_config: dict):
+ """Add connector keys."""
+ url = f"{self.base_url}/add-connector-keys/{account_name}/{connector_name}"
+ response = requests.post(url, json=connector_config)
+ return response.json()
+
+ def get_accounts(self):
+ """Get available credentials."""
+ url = f"{self.base_url}/list-accounts"
+ response = requests.get(url)
+ return response.json()
+
+ def get_credentials(self, account_name: str):
+ """Get available credentials."""
+ url = f"{self.base_url}/list-credentials/{account_name}"
+ response = requests.get(url)
+ return response.json()
+
+ def get_all_balances(self):
+ """Get all balances."""
+ url = f"{self.base_url}/get-all-balances"
+ response = requests.get(url)
+ return response.json()
diff --git a/utils/coingecko_utils.py b/backend/services/coingecko_client.py
similarity index 98%
rename from utils/coingecko_utils.py
rename to backend/services/coingecko_client.py
index 919793b..d870add 100644
--- a/utils/coingecko_utils.py
+++ b/backend/services/coingecko_client.py
@@ -5,7 +5,7 @@ import pandas as pd
import re
-class CoinGeckoUtils:
+class CoinGeckoClient:
def __init__(self):
self.connector = CoinGeckoAPI()
diff --git a/utils/miner_utils.py b/backend/services/miner_client.py
similarity index 99%
rename from utils/miner_utils.py
rename to backend/services/miner_client.py
index ca70299..2332d8c 100644
--- a/utils/miner_utils.py
+++ b/backend/services/miner_client.py
@@ -3,7 +3,7 @@ import requests
from glom import *
-class MinerUtils:
+class MinerClient:
MARKETS_ENDPOINT = "https://api.hummingbot.io/bounty/markets"
@staticmethod
diff --git a/quants_lab/strategy/__init__.py b/backend/utils/__init__.py
similarity index 100%
rename from quants_lab/strategy/__init__.py
rename to backend/utils/__init__.py
diff --git a/utils/file_templates.py b/backend/utils/file_templates.py
similarity index 92%
rename from utils/file_templates.py
rename to backend/utils/file_templates.py
index 3ad48a8..6a570b9 100644
--- a/utils/file_templates.py
+++ b/backend/utils/file_templates.py
@@ -10,9 +10,9 @@ from typing import Optional
import pandas as pd
from pydantic import Field
-from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
-from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
-from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_controller_base import (
+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,
)
@@ -100,9 +100,9 @@ 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.smart_components.strategy_frameworks.data_types import TripleBarrierConf, OrderLevel
-from hummingbot.smart_components.strategy_frameworks.directional_trading import DirectionalTradingBacktestingEngine
-from hummingbot.smart_components.utils.config_encoder_decoder import ConfigEncoderDecoder
+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__}
diff --git a/utils/optuna_database_manager.py b/backend/utils/optuna_database_manager.py
similarity index 99%
rename from utils/optuna_database_manager.py
rename to backend/utils/optuna_database_manager.py
index a124153..9293c84 100644
--- a/utils/optuna_database_manager.py
+++ b/backend/utils/optuna_database_manager.py
@@ -6,8 +6,6 @@ import pandas as pd
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
-from utils.data_manipulation import StrategyData
-
class OptunaDBManager:
def __init__(self, db_name, db_root_path: Optional[str]):
diff --git a/utils/os_utils.py b/backend/utils/os_utils.py
similarity index 94%
rename from utils/os_utils.py
rename to backend/utils/os_utils.py
index e0ff6c3..bec7293 100644
--- a/utils/os_utils.py
+++ b/backend/utils/os_utils.py
@@ -5,11 +5,10 @@ import inspect
import os
import pandas as pd
-from hummingbot.smart_components.strategy_frameworks.directional_trading import DirectionalTradingControllerBase, DirectionalTradingControllerConfigBase
import yaml
-from hummingbot.smart_components.strategy_frameworks.market_making import MarketMakingControllerBase, \
- MarketMakingControllerConfigBase
+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):
diff --git a/environment_conda.yml b/environment_conda.yml
index c66e00c..c903dd2 100644
--- a/environment_conda.yml
+++ b/environment_conda.yml
@@ -4,13 +4,13 @@ channels:
- conda-forge
dependencies:
- python=3.10
- - sqlalchemy
- - pydantic=1.9.*
+ - sqlalchemy=1.4
- pip
- pip:
- hummingbot
- - streamlit
+ - streamlit==1.33.0
- watchdog
+ - python-dotenv
- plotly
- pycoingecko
- glom
@@ -18,14 +18,11 @@ dependencies:
- statsmodels
- pandas_ta==0.3.14b
- pyyaml
- - commlib-py
- jupyter
- optuna
- optuna-dashboard
- pathlib
- - streamlit-ace
- st-pages
- streamlit-elements==0.1.*
- streamlit-authenticator
- - git+https://github.com/hummingbot/hbot-remote-client-py.git
- - git+https://github.com/hummingbot/docker-manager.git
+ - pydantic==1.10.4
diff --git a/ui_components/__init__.py b/frontend/__init__.py
similarity index 100%
rename from ui_components/__init__.py
rename to frontend/__init__.py
diff --git a/utils/__init__.py b/frontend/components/__init__.py
similarity index 100%
rename from utils/__init__.py
rename to frontend/components/__init__.py
diff --git a/frontend/components/backtesting.py b/frontend/components/backtesting.py
new file mode 100644
index 0000000..d333256
--- /dev/null
+++ b/frontend/components/backtesting.py
@@ -0,0 +1,38 @@
+import streamlit as st
+from datetime import datetime, timedelta
+
+
+def backtesting_section(inputs, backend_api_client):
+ st.write("### Backtesting")
+ c1, c2, c3, c4, c5 = st.columns(5)
+ default_end_time = datetime.now().date() - timedelta(days=1)
+ default_start_time = default_end_time - timedelta(days=2)
+ with c1:
+ start_date = st.date_input("Start Date", default_start_time)
+ with c2:
+ end_date = st.date_input("End Date", default_end_time,
+ help="End date is inclusive, make sure that you are not including the current date.")
+ with c3:
+ backtesting_resolution = st.selectbox("Backtesting Resolution", options=["1m", "3m", "5m", "15m", "30m", "1h", "1s"], index=0)
+ with c4:
+ trade_cost = st.number_input("Trade Cost (%)", min_value=0.0, value=0.06, step=0.01, format="%.2f")
+ with c5:
+ run_backtesting = st.button("Run Backtesting")
+
+ if run_backtesting:
+ start_datetime = datetime.combine(start_date, datetime.min.time())
+ end_datetime = datetime.combine(end_date, datetime.max.time())
+ backtesting_results = backend_api_client.run_backtesting(
+ start_time=int(start_datetime.timestamp()) * 1000,
+ end_time=int(end_datetime.timestamp()) * 1000,
+ backtesting_resolution=backtesting_resolution,
+ trade_cost=trade_cost / 100,
+ config=inputs,
+ )
+ if len(backtesting_results["processed_data"]) == 0:
+ st.error("No trades were executed during the backtesting period.")
+ return None
+ if len(backtesting_results["executors"]) == 0:
+ st.error("No executors were found during the backtesting period.")
+ return None
+ return backtesting_results
diff --git a/frontend/components/bot_performance_card.py b/frontend/components/bot_performance_card.py
new file mode 100644
index 0000000..c045bc9
--- /dev/null
+++ b/frontend/components/bot_performance_card.py
@@ -0,0 +1,285 @@
+import time
+
+from streamlit_elements import mui
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from frontend.components.dashboard import Dashboard
+
+from backend.services.backend_api_client import BackendAPIClient
+
+TRADES_TO_SHOW = 5
+WIDE_COL_WIDTH = 250
+MEDIUM_COL_WIDTH = 170
+SMALL_COL_WIDTH = 100
+backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+
+
+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": 'realized_pnl_quote', "headerName": 'Realized PNL ($)', "width": MEDIUM_COL_WIDTH, "editable": False},
+ {"field": 'unrealized_pnl_quote', "headerName": 'Unrealized PNL ($)', "width": MEDIUM_COL_WIDTH, "editable": False},
+ {"field": 'global_pnl_quote', "headerName": 'NET PNL ($)', "width": MEDIUM_COL_WIDTH, "editable": False},
+ {"field": 'volume_traded', "headerName": 'Volume ($)', "width": MEDIUM_COL_WIDTH, "editable": False},
+ {"field": 'open_order_volume', "headerName": 'Open Order Volume ($)', "width": MEDIUM_COL_WIDTH, "editable": False},
+ {"field": 'imbalance', "headerName": 'Imbalance ($)', "width": MEDIUM_COL_WIDTH, "editable": False},
+ ]
+ _active_controller_config_selected = []
+ _stopped_controller_config_selected = []
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+
+ 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)
+ 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
+ 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:
+ bot_data = bot_status.get("data")
+ 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), {})
+ 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("imbalance", 0)
+ controller_info = {
+ "id": controller,
+ "realized_pnl_quote": realized_pnl_quote,
+ "unrealized_pnl_quote": unrealized_pnl_quote,
+ "global_pnl_quote": global_pnl_quote,
+ "volume_traded": volume_traded,
+ "open_order_volume": open_order_volume,
+ "imbalance": imbalance,
+ }
+ 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")
+ 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"})
\ No newline at end of file
diff --git a/ui_components/bots_file_explorer.py b/frontend/components/bots_file_explorer.py
similarity index 90%
rename from ui_components/bots_file_explorer.py
rename to frontend/components/bots_file_explorer.py
index 6b9da2e..a91cd0c 100644
--- a/ui_components/bots_file_explorer.py
+++ b/frontend/components/bots_file_explorer.py
@@ -1,9 +1,9 @@
from streamlit_elements import mui
import constants
-from ui_components.file_explorer_base import FileExplorerBase
-from utils.os_utils import get_directories_from_directory, get_python_files_from_directory, \
+from backend.utils.os_utils import get_directories_from_directory, get_python_files_from_directory, \
get_yml_files_from_directory, get_log_files_from_directory
+from frontend.components.file_explorer_base import FileExplorerBase
class BotsFileExplorer(FileExplorerBase):
diff --git a/ui_components/card.py b/frontend/components/card.py
similarity index 95%
rename from ui_components/card.py
rename to frontend/components/card.py
index 10b9a90..557a290 100644
--- a/ui_components/card.py
+++ b/frontend/components/card.py
@@ -1,5 +1,5 @@
from streamlit_elements import mui
-from ui_components.dashboard import Dashboard
+from frontend.components.dashboard import Dashboard
class Card(Dashboard.Item):
diff --git a/frontend/components/config_loader.py b/frontend/components/config_loader.py
new file mode 100644
index 0000000..3f5ff1f
--- /dev/null
+++ b/frontend/components/config_loader.py
@@ -0,0 +1,17 @@
+import streamlit as st
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+
+from backend.services.backend_api_client import BackendAPIClient
+
+backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+
+
+def get_default_config_loader(controller_name: str):
+ use_default_config = st.checkbox("Use default config", value=True)
+ all_configs = backend_api_client.get_all_controllers_config()
+ if use_default_config:
+ st.session_state["default_config"] = {}
+ else:
+ configs = [config for config in all_configs if config["controller_name"] == controller_name]
+ default_config = st.selectbox("Select a config", [config["id"] for config in configs])
+ st.session_state["default_config"] = next((config for config in all_configs if config["id"] == default_config), {})
diff --git a/ui_components/controllers_file_explorer.py b/frontend/components/controllers_file_explorer.py
similarity index 90%
rename from ui_components/controllers_file_explorer.py
rename to frontend/components/controllers_file_explorer.py
index 3940c77..b38bebb 100644
--- a/ui_components/controllers_file_explorer.py
+++ b/frontend/components/controllers_file_explorer.py
@@ -1,8 +1,8 @@
from streamlit_elements import mui
import constants
-from ui_components.file_explorer_base import FileExplorerBase
-from utils.os_utils import get_python_files_from_directory, load_controllers
+from backend.utils.os_utils import load_controllers
+from frontend.components.file_explorer_base import FileExplorerBase
class ControllersFileExplorer(FileExplorerBase):
diff --git a/ui_components/dashboard.py b/frontend/components/dashboard.py
similarity index 100%
rename from ui_components/dashboard.py
rename to frontend/components/dashboard.py
diff --git a/ui_components/datagrid.py b/frontend/components/datagrid.py
similarity index 100%
rename from ui_components/datagrid.py
rename to frontend/components/datagrid.py
diff --git a/frontend/components/dca_distribution.py b/frontend/components/dca_distribution.py
new file mode 100644
index 0000000..a6cd060
--- /dev/null
+++ b/frontend/components/dca_distribution.py
@@ -0,0 +1,79 @@
+import streamlit as st
+
+from frontend.components.st_inputs import get_distribution, normalize, distribution_inputs
+
+
+def get_dca_distribution_inputs():
+ 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])
+ 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
+ ts_ap = default_config.get("trailing_stop", {}).get("activation_price", 0.018) * 100
+ ts_delta = default_config.get("trailing_stop", {}).get("trailing_delta", 0.002) * 100
+ levels_def = len(dca_spreads)
+ c1, c2 = st.columns([0.67, 0.33])
+ with c1:
+ st.header("DCA Distribution")
+ buy_order_levels = st.number_input("Number of Order Levels", min_value=1, value=levels_def,
+ help="Enter the number of order levels (e.g., 2).")
+
+ if buy_order_levels > levels_def:
+ dca_spreads += [0.01 + max(dca_spreads)] * (buy_order_levels - levels_def)
+ dca_amounts += [0.2 + max(dca_amounts)] * (buy_order_levels - levels_def)
+ elif buy_order_levels < levels_def:
+ dca_spreads = dca_spreads[:buy_order_levels]
+ dca_amounts = dca_amounts[:buy_order_levels]
+ col_spreads, col_amounts = st.columns(2)
+ with col_spreads:
+ buy_spread_dist_type, buy_spread_start, buy_spread_base, buy_spread_scaling, buy_spread_step, buy_spread_ratio, buy_manual_spreads = distribution_inputs(
+ col_spreads, "Spread", buy_order_levels, dca_spreads)
+ with col_amounts:
+ buy_amount_dist_type, buy_amount_start, buy_amount_base, buy_amount_scaling, buy_amount_step, buy_amount_ratio, buy_manual_amounts = distribution_inputs(
+ col_amounts, "Amount", buy_order_levels, dca_amounts)
+
+ # Generate distributions
+ spread_distributions = get_distribution(buy_spread_dist_type, buy_order_levels, buy_spread_start,
+ buy_spread_base, buy_spread_scaling, buy_spread_step, buy_spread_ratio,
+ buy_manual_spreads)
+
+ amount_distributions = get_distribution(buy_amount_dist_type, buy_order_levels, buy_amount_start,
+ buy_amount_base, buy_amount_scaling,
+ buy_amount_step, buy_amount_ratio, buy_manual_amounts)
+
+ # Normalize and calculate order amounts
+ orders_amount_normalized = normalize(amount_distributions)
+ spreads_normalized = [spread - spread_distributions[0] for spread in spread_distributions]
+ st.write("---")
+ # c1, c2, c3, c4, c5 = st.columns(5)
+ with c2:
+ st.header("Risk Management")
+ sl = st.number_input("Stop Loss (%)", min_value=0.0, max_value=100.0, value=sl, step=0.1,
+ help="Enter the stop loss as a percentage (e.g., 2.0 for 2%).") / 100
+ # with c2:
+ tp = st.number_input("Take Profit (%)", min_value=0.0, max_value=100.0, value=tp, step=0.1,
+ help="Enter the take profit as a percentage (e.g., 3.0 for 3%).") / 100
+ # with c3:
+ time_limit = st.number_input("Time Limit (minutes)", min_value=0, value=time_limit,
+ help="Enter the time limit in minutes (e.g., 360 for 6 hours).") * 60
+ # with c4:
+ ts_ap = st.number_input("Trailing Stop Act. Price (%)", min_value=0.0, max_value=100.0, value=ts_ap,
+ step=0.1,
+ help="Enter the tr ailing stop activation price as a percentage (e.g., 1.0 for 1%).") / 100
+ # with c5:
+ ts_delta = st.number_input("Trailing Stop Delta (%)", min_value=0.0, max_value=100.0, value=ts_delta, step=0.1,
+ help="Enter the trailing stop delta as a percentage (e.g., 0.3 for 0.3%).") / 100
+
+ return {
+ "dca_spreads": [spread / 100 for spread in spreads_normalized],
+ "dca_amounts": orders_amount_normalized,
+ "stop_loss": sl,
+ "take_profit": tp,
+ "time_limit": time_limit,
+ "trailing_stop": {
+ "activation_price": ts_ap,
+ "trailing_delta": ts_delta
+ },
+ }
diff --git a/frontend/components/deploy_v2_with_controllers.py b/frontend/components/deploy_v2_with_controllers.py
new file mode 100644
index 0000000..9faae93
--- /dev/null
+++ b/frontend/components/deploy_v2_with_controllers.py
@@ -0,0 +1,110 @@
+import time
+import streamlit as st
+import pandas as pd
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+
+
+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 = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+ self._controller_configs_available = self._backend_api_client.get_all_controllers_config()
+ self._controller_config_selected = []
+ self._bot_name = None
+ self._image_name = "dardonacci/hummingbot:latest"
+ self._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,
+ "config_update_interval": 20,
+ "script_file_name": "v2_with_controllers.py",
+ "time_to_cash_out": None,
+ }
+ }
+
+ 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()
\ No newline at end of file
diff --git a/frontend/components/directional_trading_general_inputs.py b/frontend/components/directional_trading_general_inputs.py
new file mode 100644
index 0000000..5f7b03a
--- /dev/null
+++ b/frontend/components/directional_trading_general_inputs.py
@@ -0,0 +1,48 @@
+import streamlit as st
+
+
+def get_directional_trading_general_inputs():
+ 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", {})
+ connector_name = default_config.get("connector_name", "kucoin")
+ trading_pair = default_config.get("trading_pair", "WLD-USDT")
+ leverage = default_config.get("leverage", 20)
+ total_amount_quote = default_config.get("total_amount_quote", 1000)
+ max_executors_per_side = default_config.get("max_executors_per_side", 5)
+ cooldown_time = default_config.get("cooldown_time", 60 * 60) / 60
+ position_mode = 0 if default_config.get("position_mode", "HEDGE") == "HEDGE" else 1
+ candles_connector_name = default_config.get("candles_connector_name", "kucoin")
+ candles_trading_pair = default_config.get("candles_trading_pair", "WLD-USDT")
+ interval = default_config.get("interval", "3m")
+ intervals = ["1m", "3m", "5m", "15m", "1h", "4h", "1d", "1s"]
+ interval_index = intervals.index(interval)
+
+ with c1:
+ connector_name = st.text_input("Connector", value=connector_name,
+ help="Enter the name of the exchange to trade on (e.g., binance_perpetual).")
+ candles_connector_name = st.text_input("Candles Connector", value=candles_connector_name,
+ help="Enter the name of the exchange to get candles from (e.g., binance_perpetual).")
+ with c2:
+ trading_pair = st.text_input("Trading Pair", value=trading_pair,
+ help="Enter the trading pair to trade on (e.g., WLD-USDT).")
+ candles_trading_pair = st.text_input("Candles Trading Pair", value=candles_trading_pair,
+ help="Enter the trading pair to get candles for (e.g., WLD-USDT).")
+ with c3:
+ leverage = st.number_input("Leverage", value=leverage,
+ help="Set the leverage to use for trading (e.g., 20 for 20x leverage). Set it to 1 for spot trading.")
+ interval = st.selectbox("Candles Interval", ("1m", "3m", "5m", "15m", "1h", "4h", "1d"), index=interval_index,
+ help="Enter the interval for candles (e.g., 1m).")
+ with c4:
+ total_amount_quote = st.number_input("Total amount of quote", value=total_amount_quote,
+ help="Enter the total amount in quote asset to use for trading (e.g., 1000).")
+ with c5:
+ max_executors_per_side = st.number_input("Max Executors Per Side", value=max_executors_per_side,
+ help="Enter the maximum number of executors per side (e.g., 5).")
+ with c6:
+ cooldown_time = st.number_input("Cooldown Time (minutes)", value=cooldown_time,
+ help="Time between accepting a new signal in minutes (e.g., 60).") * 60
+ with c7:
+ position_mode = st.selectbox("Position Mode", ("HEDGE", "ONEWAY"), index=position_mode,
+ help="Enter the position mode (HEDGE/ONEWAY).")
+ return connector_name, trading_pair, leverage, total_amount_quote, max_executors_per_side, cooldown_time, position_mode, candles_connector_name, candles_trading_pair, interval
diff --git a/ui_components/editor.py b/frontend/components/editor.py
similarity index 98%
rename from ui_components/editor.py
rename to frontend/components/editor.py
index df65be6..ea4a454 100644
--- a/ui_components/editor.py
+++ b/frontend/components/editor.py
@@ -2,7 +2,7 @@ from functools import partial
import streamlit as st
from streamlit_elements import mui, editor, sync, lazy, event
-from utils.os_utils import save_file
+from backend.utils.os_utils import save_file
from .dashboard import Dashboard
diff --git a/frontend/components/executors_distribution.py b/frontend/components/executors_distribution.py
new file mode 100644
index 0000000..0b77495
--- /dev/null
+++ b/frontend/components/executors_distribution.py
@@ -0,0 +1,68 @@
+import streamlit as st
+from frontend.components.st_inputs import get_distribution, normalize, distribution_inputs
+
+
+def get_executors_distribution_inputs(default_spreads=[0.01, 0.02], default_amounts=[0.2, 0.8]):
+ default_config = st.session_state.get("default_config", {})
+ buy_spreads = default_config.get("buy_spreads", default_spreads)
+ sell_spreads = default_config.get("sell_spreads", default_spreads)
+ buy_amounts_pct = default_config.get("buy_amounts_pct", default_amounts)
+ sell_amounts_pct = default_config.get("sell_amounts_pct", default_amounts)
+ buy_order_levels_def = len(buy_spreads)
+ sell_order_levels_def = len(sell_spreads)
+ with st.expander("Executors Configuration", expanded=True):
+ col_buy, col_sell = st.columns(2)
+ with col_buy:
+ st.header("Buy Order Settings")
+ buy_order_levels = st.number_input("Number of Buy Order Levels", min_value=1, value=buy_order_levels_def,
+ help="Enter the number of buy order levels (e.g., 2).")
+ with col_sell:
+ st.header("Sell Order Settings")
+ sell_order_levels = st.number_input("Number of Sell Order Levels", min_value=1, value=sell_order_levels_def,
+ help="Enter the number of sell order levels (e.g., 2).")
+ if buy_order_levels > buy_order_levels_def:
+ buy_spreads += [0.01 + max(buy_spreads)] * (buy_order_levels - buy_order_levels_def)
+ buy_amounts_pct += [0.2 + max(buy_amounts_pct)] * (buy_order_levels - buy_order_levels_def)
+ elif buy_order_levels < buy_order_levels_def:
+ buy_spreads = buy_spreads[:buy_order_levels]
+ buy_amounts_pct = buy_amounts_pct[:buy_order_levels]
+ if sell_order_levels > sell_order_levels_def:
+ sell_spreads += [0.01 + max(sell_spreads)] * (sell_order_levels - sell_order_levels_def)
+ sell_amounts_pct += [0.2 + max(sell_amounts_pct)] * (sell_order_levels - sell_order_levels_def)
+ elif sell_order_levels < sell_order_levels_def:
+ sell_spreads = sell_spreads[:sell_order_levels]
+ sell_amounts_pct = sell_amounts_pct[:sell_order_levels]
+ col_buy_spreads, col_buy_amounts, col_sell_spreads, col_sell_amounts = st.columns(4)
+ with col_buy_spreads:
+ buy_spread_dist_type, buy_spread_start, buy_spread_base, buy_spread_scaling, buy_spread_step, buy_spread_ratio, buy_manual_spreads = distribution_inputs(
+ col_buy_spreads, "Spread", buy_order_levels, buy_spreads)
+ with col_buy_amounts:
+ buy_amount_dist_type, buy_amount_start, buy_amount_base, buy_amount_scaling, buy_amount_step, buy_amount_ratio, buy_manual_amounts = distribution_inputs(
+ col_buy_amounts, "Amount", buy_order_levels, buy_amounts_pct)
+ with col_sell_spreads:
+ sell_spread_dist_type, sell_spread_start, sell_spread_base, sell_spread_scaling, sell_spread_step, sell_spread_ratio, sell_manual_spreads = distribution_inputs(
+ col_sell_spreads, "Spread", sell_order_levels, sell_spreads)
+ with col_sell_amounts:
+ sell_amount_dist_type, sell_amount_start, sell_amount_base, sell_amount_scaling, sell_amount_step, sell_amount_ratio, sell_manual_amounts = distribution_inputs(
+ col_sell_amounts, "Amount", sell_order_levels, sell_amounts_pct)
+
+ # Generate distributions
+ buy_spread_distributions = get_distribution(buy_spread_dist_type, buy_order_levels, buy_spread_start,
+ buy_spread_base, buy_spread_scaling, buy_spread_step, buy_spread_ratio,
+ buy_manual_spreads)
+ sell_spread_distributions = get_distribution(sell_spread_dist_type, sell_order_levels, sell_spread_start,
+ sell_spread_base, sell_spread_scaling, sell_spread_step,
+ sell_spread_ratio, sell_manual_spreads)
+
+ buy_amount_distributions = get_distribution(buy_amount_dist_type, buy_order_levels, buy_amount_start, buy_amount_base, buy_amount_scaling,
+ buy_amount_step, buy_amount_ratio, buy_manual_amounts)
+ sell_amount_distributions = get_distribution(sell_amount_dist_type, sell_order_levels, sell_amount_start, sell_amount_base,
+ 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:]]
+ 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/ui_components/exited_bot_card.py b/frontend/components/exited_bot_card.py
similarity index 88%
rename from ui_components/exited_bot_card.py
rename to frontend/components/exited_bot_card.py
index 6d4c253..df40ae7 100644
--- a/ui_components/exited_bot_card.py
+++ b/frontend/components/exited_bot_card.py
@@ -1,11 +1,8 @@
from docker_manager import DockerManager
-from streamlit_elements import mui, lazy
-from ui_components.dashboard import Dashboard
-import streamlit as st
-import time
+from streamlit_elements import mui
+from frontend.components.dashboard import Dashboard
-from utils import os_utils
-from utils.os_utils import get_python_files_from_directory, get_yml_files_from_directory
+from backend.utils import os_utils
class ExitedBotCard(Dashboard.Item):
diff --git a/ui_components/file_explorer_base.py b/frontend/components/file_explorer_base.py
similarity index 90%
rename from ui_components/file_explorer_base.py
rename to frontend/components/file_explorer_base.py
index 22d1b85..4d64318 100644
--- a/ui_components/file_explorer_base.py
+++ b/frontend/components/file_explorer_base.py
@@ -1,7 +1,7 @@
import streamlit as st
-from streamlit_elements import mui, elements
+from streamlit_elements import mui
-from utils.os_utils import load_file, remove_file
+from backend.utils.os_utils import remove_file, load_file
from .dashboard import Dashboard
@@ -29,8 +29,9 @@ class FileExplorerBase(Dashboard.Item):
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}
+ 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:
diff --git a/ui_components/launch_bot_card.py b/frontend/components/launch_bot_card.py
similarity index 98%
rename from ui_components/launch_bot_card.py
rename to frontend/components/launch_bot_card.py
index 6cf9f7f..c435518 100644
--- a/ui_components/launch_bot_card.py
+++ b/frontend/components/launch_bot_card.py
@@ -6,7 +6,7 @@ import streamlit as st
from streamlit_elements import mui, lazy
import constants
-from utils.os_utils import get_directories_from_directory
+from backend.utils.os_utils import get_directories_from_directory
from .dashboard import Dashboard
diff --git a/ui_components/launch_broker_card.py b/frontend/components/launch_broker_card.py
similarity index 100%
rename from ui_components/launch_broker_card.py
rename to frontend/components/launch_broker_card.py
diff --git a/ui_components/launch_master_bot_card.py b/frontend/components/launch_master_bot_card.py
similarity index 100%
rename from ui_components/launch_master_bot_card.py
rename to frontend/components/launch_master_bot_card.py
diff --git a/frontend/components/launch_strategy_v2.py b/frontend/components/launch_strategy_v2.py
new file mode 100644
index 0000000..810957a
--- /dev/null
+++ b/frontend/components/launch_strategy_v2.py
@@ -0,0 +1,160 @@
+import time
+
+import streamlit as st
+from streamlit_elements import mui, lazy
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+from .dashboard import Dashboard
+
+
+class LaunchStrategyV2(Dashboard.Item):
+ DEFAULT_ROWS = []
+ DEFAULT_COLUMNS = [
+ {"field": 'id', "headerName": 'ID', "width": 230},
+ {"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 = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+ self._controller_configs_available = self._backend_api_client.get_all_controllers_config()
+ self._controller_config_selected = None
+ self._bot_name = None
+ self._image_name = "dardonacci/hummingbot:latest"
+ self._credentials = "master_account"
+
+ 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 launch_new_bot(self):
+ if self._bot_name and self._image_name and len(self._controller_config_selected) > 0:
+ 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,
+ "config_update_interval": 10,
+ "script_file_name": "v2_with_controllers.py",
+ "time_to_cash_out": None,
+ }
+ }
+
+ 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):
+ with mui.Paper(key=self._key,
+ sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"},
+ elevation=1):
+ with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False):
+ mui.Typography("π Select the controller configs to launch", variant="h5")
+
+ with mui.Grid(container=True, spacing=2, sx={"padding": "10px 15px 10px 15px"}):
+ with mui.Grid(item=True, xs=8):
+ mui.Alert(
+ "The new instance will contain the credentials configured in the following base instance:",
+ severity="info")
+ with mui.Grid(item=True, xs=4):
+ 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=4):
+ mui.TextField(label="Instance Name", variant="outlined", onChange=lazy(self._set_bot_name),
+ sx={"width": "100%"})
+ with mui.Grid(item=True, xs=4):
+ available_images = self._backend_api_client.get_available_images("hummingbot")
+ with mui.FormControl(variant="standard", sx={"width": "100%"}):
+ mui.FormHelperText("Available Images")
+ with mui.Select(label="Hummingbot Image", defaultValue="dardonacci/hummingbot:latest",
+ variant="standard", onChange=lazy(self._set_image_name)):
+ for image in available_images:
+ mui.MenuItem(image, value=image)
+ with mui.Grid(item=True, xs=4):
+ with mui.Button(onClick=self.launch_new_bot,
+ variant="outlined",
+ color="success",
+ sx={"width": "100%", "height": "100%"}):
+ mui.icon.AddCircleOutline()
+ mui.Typography("Create")
+ all_controllers_config = self._backend_api_client.get_all_controllers_config()
+ data = []
+ for config in all_controllers_config:
+ 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({"id": config["id"], "controller_name": config["controller_name"],
+ "controller_type": config["controller_type"],
+ "connector_name": connector_name,
+ "trading_pair": trading_pair,
+ "total_amount_quote": total_amount_quote,
+ "max_loss_quote": total_amount_quote * stop_loss / 2,
+ "stop_loss": stop_loss,
+ "take_profit": take_profit,
+ "trailing_stop": str(trailing_stop["activation_price"]) + " / " +
+ str(trailing_stop["trailing_delta"]),
+ "time_limit": time_limit})
+
+ with mui.Grid(item=True, xs=12):
+ mui.Alert("Select the controller configs to deploy", severity="info")
+ with mui.Paper(key=self._key,
+ sx={"display": "flex", "flexDirection": "column", "borderRadius": 3,
+ "overflow": "hidden", "height": 1000},
+ elevation=1):
+ with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False):
+ mui.icon.ViewCompact()
+ mui.Typography("Controllers Config")
+ with mui.Box(sx={"flex": 1, "minHeight": 3}):
+ mui.DataGrid(
+ columns=self.DEFAULT_COLUMNS,
+ rows=data,
+ pageSize=15,
+ rowsPerPageOptions=[15],
+ checkboxSelection=True,
+ disableSelectionOnClick=True,
+ onSelectionModelChange=self._handle_row_selection,
+ )
diff --git a/frontend/components/market_making_general_inputs.py b/frontend/components/market_making_general_inputs.py
new file mode 100644
index 0000000..47b7f5e
--- /dev/null
+++ b/frontend/components/market_making_general_inputs.py
@@ -0,0 +1,54 @@
+import streamlit as st
+
+
+def get_market_making_general_inputs(custom_candles=False):
+ 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", {})
+ connector_name = default_config.get("connector_name", "kucoin")
+ trading_pair = default_config.get("trading_pair", "WLD-USDT")
+ leverage = default_config.get("leverage", 20)
+ total_amount_quote = default_config.get("total_amount_quote", 1000)
+ position_mode = 0 if default_config.get("position_mode", "HEDGE") == "HEDGE" else 1
+ cooldown_time = default_config.get("cooldown_time", 60 * 60) / 60
+ executor_refresh_time = default_config.get("executor_refresh_time", 60 * 60) / 60
+ candles_connector = None
+ candles_trading_pair = None
+ interval = None
+ with c1:
+ connector_name = st.text_input("Connector", value=connector_name,
+ help="Enter the name of the exchange to trade on (e.g., binance_perpetual).")
+ with c2:
+ trading_pair = st.text_input("Trading Pair", value=trading_pair,
+ help="Enter the trading pair to trade on (e.g., WLD-USDT).")
+ with c3:
+ leverage = st.number_input("Leverage", value=leverage,
+ help="Set the leverage to use for trading (e.g., 20 for 20x leverage). Set it to 1 for spot trading.")
+ with c4:
+ total_amount_quote = st.number_input("Total amount of quote", value=total_amount_quote,
+ help="Enter the total amount in quote asset to use for trading (e.g., 1000).")
+ with c5:
+ position_mode = st.selectbox("Position Mode", ("HEDGE", "ONEWAY"), index=position_mode,
+ help="Enter the position mode (HEDGE/ONEWAY).")
+ with c6:
+ cooldown_time = st.number_input("Stop Loss Cooldown Time (minutes)", value=cooldown_time,
+ help="Specify the cooldown time in minutes after having a stop loss (e.g., 60).") * 60
+ with c7:
+ executor_refresh_time = st.number_input("Executor Refresh Time (minutes)", value=executor_refresh_time,
+ help="Enter the refresh time in minutes for executors (e.g., 60).") * 60
+ if custom_candles:
+ candles_connector = default_config.get("candles_connector", "kucoin")
+ candles_trading_pair = default_config.get("candles_trading_pair", "WLD-USDT")
+ interval = default_config.get("interval", "3m")
+ intervals = ["1m", "3m", "5m", "15m", "1h", "4h", "1d"]
+ interval_index = intervals.index(interval)
+ with c1:
+ candles_connector = st.text_input("Candles Connector", value=candles_connector,
+ help="Enter the name of the exchange to get candles from (e.g., binance_perpetual).")
+ with c2:
+ candles_trading_pair = st.text_input("Candles Trading Pair", value=candles_trading_pair,
+ help="Enter the trading pair to get candles for (e.g., WLD-USDT).")
+ with c3:
+ interval = st.selectbox("Candles Interval", intervals, index=interval_index,
+ help="Enter the interval for candles (e.g., 1m).")
+ return connector_name, trading_pair, leverage, total_amount_quote, position_mode, cooldown_time, executor_refresh_time, candles_connector, candles_trading_pair, interval
diff --git a/ui_components/media_player.py b/frontend/components/media_player.py
similarity index 100%
rename from ui_components/media_player.py
rename to frontend/components/media_player.py
diff --git a/ui_components/optimization_creation_card.py b/frontend/components/optimization_creation_card.py
similarity index 96%
rename from ui_components/optimization_creation_card.py
rename to frontend/components/optimization_creation_card.py
index ee9a9b4..78765ac 100644
--- a/ui_components/optimization_creation_card.py
+++ b/frontend/components/optimization_creation_card.py
@@ -2,8 +2,8 @@ from streamlit_elements import mui, lazy
import datetime
import constants
-from utils.file_templates import strategy_optimization_template
-from utils.os_utils import save_file, load_controllers
+from backend.utils.file_templates import strategy_optimization_template
+from backend.utils.os_utils import load_controllers, save_file
from .dashboard import Dashboard
diff --git a/ui_components/optimization_run_card.py b/frontend/components/optimization_run_card.py
similarity index 97%
rename from ui_components/optimization_run_card.py
rename to frontend/components/optimization_run_card.py
index 91aa086..531cec3 100644
--- a/ui_components/optimization_run_card.py
+++ b/frontend/components/optimization_run_card.py
@@ -4,8 +4,7 @@ import optuna
from streamlit_elements import mui, lazy
import constants
-from utils.os_utils import get_python_files_from_directory, \
- get_function_from_file
+from backend.utils.os_utils import get_function_from_file, get_python_files_from_directory
from .dashboard import Dashboard
diff --git a/ui_components/optimizations_file_explorer.py b/frontend/components/optimizations_file_explorer.py
similarity index 85%
rename from ui_components/optimizations_file_explorer.py
rename to frontend/components/optimizations_file_explorer.py
index c0354ab..901b93a 100644
--- a/ui_components/optimizations_file_explorer.py
+++ b/frontend/components/optimizations_file_explorer.py
@@ -1,8 +1,8 @@
from streamlit_elements import mui
import constants
-from ui_components.file_explorer_base import FileExplorerBase
-from utils.os_utils import get_python_files_from_directory
+from backend.utils.os_utils import get_python_files_from_directory
+from frontend.components.file_explorer_base import FileExplorerBase
class OptimizationsStrategiesFileExplorer(FileExplorerBase):
diff --git a/frontend/components/risk_management.py b/frontend/components/risk_management.py
new file mode 100644
index 0000000..bcf3ee1
--- /dev/null
+++ b/frontend/components/risk_management.py
@@ -0,0 +1,38 @@
+import streamlit as st
+from hummingbot.connector.connector_base import OrderType
+
+
+def get_risk_management_inputs():
+ default_config = st.session_state.get("default_config", {})
+ sl = default_config.get("stop_loss", 0.05) * 100
+ tp = default_config.get("take_profit", 0.02) * 100
+ time_limit = default_config.get("time_limit", 60 * 12 * 60) // 60
+ ts_ap = default_config.get("trailing_stop", {}).get("activation_price", 0.018) * 100
+ ts_delta = default_config.get("trailing_stop", {}).get("trailing_delta", 0.002) * 100
+ take_profit_order_type = OrderType(default_config.get("take_profit_order_type", 2))
+ order_types = [OrderType.LIMIT, OrderType.MARKET]
+ order_type_index = order_types.index(take_profit_order_type)
+ with st.expander("Risk Management", expanded=True):
+ c1, c2, c3, c4, c5, c6 = st.columns(6)
+
+ with c1:
+ sl = st.number_input("Stop Loss (%)", min_value=0.0, max_value=100.0, value=sl, step=0.1,
+ help="Enter the stop loss as a percentage (e.g., 2.0 for 2%).") / 100
+ with c2:
+ tp = st.number_input("Take Profit (%)", min_value=0.0, max_value=100.0, value=tp, step=0.1,
+ help="Enter the take profit as a percentage (e.g., 3.0 for 3%).") / 100
+ with c3:
+ time_limit = st.number_input("Time Limit (minutes)", min_value=0, value=time_limit,
+ help="Enter the time limit in minutes (e.g., 360 for 6 hours).") * 60
+ with c4:
+ ts_ap = st.number_input("Trailing Stop Act. Price (%)", min_value=0.0, max_value=100.0, value=ts_ap,
+ step=0.1,
+ help="Enter the tr ailing stop activation price as a percentage (e.g., 1.0 for 1%).") / 100
+ with c5:
+ ts_delta = st.number_input("Trailing Stop Delta (%)", min_value=0.0, max_value=100.0, value=ts_delta, step=0.1,
+ help="Enter the trailing stop delta as a percentage (e.g., 0.3 for 0.3%).") / 100
+ with c6:
+ take_profit_order_type = st.selectbox("Take Profit Order Type", (OrderType.LIMIT, OrderType.MARKET),
+ index=order_type_index,
+ help="Enter the order type for taking profit (LIMIT/MARKET).")
+ return sl, tp, time_limit, ts_ap, ts_delta, take_profit_order_type
diff --git a/frontend/components/save_config.py b/frontend/components/save_config.py
new file mode 100644
index 0000000..89896d7
--- /dev/null
+++ b/frontend/components/save_config.py
@@ -0,0 +1,22 @@
+import streamlit as st
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+
+
+def render_save_config(controller_name: str, config_data: dict):
+ st.write("### Upload Config to BackendAPI")
+ c1, c2, c3 = st.columns([1, 1, 0.5])
+ connector = config_data.get("connector_name", "")
+ trading_pair = config_data.get("trading_pair", "")
+ with c1:
+ config_base = st.text_input("Config Base", value=f"{controller_name}-{connector}-{trading_pair.split('-')[0]}")
+ with c2:
+ config_tag = st.text_input("Config Tag", value="1.1")
+ with c3:
+ upload_config_to_backend = st.button("Upload")
+ if upload_config_to_backend:
+ config_data["id"] = f"{config_base}-{config_tag}"
+ backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+ backend_api_client.add_controller_config(config_data)
+ st.success("Config uploaded successfully!")
diff --git a/frontend/components/st_inputs.py b/frontend/components/st_inputs.py
new file mode 100644
index 0000000..bd1d904
--- /dev/null
+++ b/frontend/components/st_inputs.py
@@ -0,0 +1,79 @@
+from _decimal import Decimal
+from math import exp
+
+from hummingbot.strategy_v2.utils.distributions import Distributions
+
+
+def normalize(values):
+ total = sum(values)
+ return [val / total for val in values]
+
+
+def distribution_inputs(column, dist_type_name, levels=3, default_values=None):
+ if dist_type_name == "Spread":
+ dist_type = column.selectbox(
+ f"Type of {dist_type_name} Distribution",
+ ("Manual", "GeoCustom", "Geometric", "Fibonacci", "Logarithmic", "Arithmetic", "Linear"),
+ key=f"{column}_{dist_type_name.lower()}_dist_type",
+ # Set the default value
+ )
+ else:
+ dist_type = column.selectbox(
+ f"Type of {dist_type_name} Distribution",
+ ("Manual", "Geometric", "Fibonacci", "Logarithmic", "Arithmetic"),
+ key=f"{column}_{dist_type_name.lower()}_dist_type",
+ # Set the default value
+ )
+ base, scaling_factor, step, ratio, manual_values = None, None, None, None, None
+
+ if dist_type != "Manual":
+ start = column.number_input(f"{dist_type_name} Start Value", value=1.0,
+ key=f"{column}_{dist_type_name.lower()}_start")
+ if dist_type == "Logarithmic":
+ base = column.number_input(f"{dist_type_name} Log Base", value=exp(1),
+ key=f"{column}_{dist_type_name.lower()}_base")
+ scaling_factor = column.number_input(f"{dist_type_name} Scaling Factor", value=2.0,
+ key=f"{column}_{dist_type_name.lower()}_scaling")
+ elif dist_type == "Arithmetic":
+ step = column.number_input(f"{dist_type_name} Step", value=0.3,
+ key=f"{column}_{dist_type_name.lower()}_step")
+ elif dist_type == "Geometric":
+ ratio = column.number_input(f"{dist_type_name} Ratio", value=2.0,
+ key=f"{column}_{dist_type_name.lower()}_ratio")
+ elif dist_type == "GeoCustom":
+ ratio = column.number_input(f"{dist_type_name} Ratio", value=2.0,
+ key=f"{column}_{dist_type_name.lower()}_ratio")
+ elif dist_type == "Linear":
+ step = column.number_input(f"{dist_type_name} End", value=1.0,
+ key=f"{column}_{dist_type_name.lower()}_end")
+ else:
+ if default_values:
+ manual_values = [column.number_input(f"{dist_type_name} for level {i + 1}", value=value * 100.0,
+ key=f"{column}_{dist_type_name.lower()}_{i}") for i, value in
+ enumerate(default_values)]
+ else:
+ manual_values = [column.number_input(f"{dist_type_name} for level {i + 1}", value=i + 1.0,
+ key=f"{column}_{dist_type_name.lower()}_{i}") for i, value in range(levels)]
+ start = None # As start is not relevant for Manual type
+
+ return dist_type, start, base, scaling_factor, step, ratio, manual_values
+
+
+def get_distribution(dist_type, n_levels, start, base=None, scaling_factor=None, step=None, ratio=None,
+ manual_values=None):
+ distribution = []
+ if dist_type == "Manual":
+ distribution = manual_values
+ elif dist_type == "Linear":
+ distribution = Distributions.linear(n_levels, start, step)
+ elif dist_type == "Fibonacci":
+ distribution = Distributions.fibonacci(n_levels, start)
+ elif dist_type == "Logarithmic":
+ distribution = Distributions.logarithmic(n_levels, base, scaling_factor, start)
+ elif dist_type == "Arithmetic":
+ distribution = Distributions.arithmetic(n_levels, start, step)
+ elif dist_type == "Geometric":
+ distribution = Distributions.geometric(n_levels, start, ratio)
+ elif dist_type == "GeoCustom":
+ distribution = [Decimal("0")] + Distributions.geometric(n_levels - 1, start, ratio)
+ return [float(val) for val in distribution]
diff --git a/pages/position_builder/README.md b/frontend/pages/__init__.py
similarity index 100%
rename from pages/position_builder/README.md
rename to frontend/pages/__init__.py
diff --git a/frontend/pages/backtesting/__init__.py b/frontend/pages/backtesting/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/backtest_analyze/README.md b/frontend/pages/backtesting/analyze/README.md
similarity index 100%
rename from pages/backtest_analyze/README.md
rename to frontend/pages/backtesting/analyze/README.md
diff --git a/frontend/pages/backtesting/analyze/__init__.py b/frontend/pages/backtesting/analyze/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/backtest_analyze/analyze.py b/frontend/pages/backtesting/analyze/analyze.py
similarity index 95%
rename from pages/backtest_analyze/analyze.py
rename to frontend/pages/backtesting/analyze/analyze.py
index 2a55573..791cd97 100644
--- a/pages/backtest_analyze/analyze.py
+++ b/frontend/pages/backtesting/analyze/analyze.py
@@ -1,8 +1,8 @@
from hummingbot.core.data_type.common import PositionMode, TradeType, OrderType
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
-from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel, TripleBarrierConf
-from hummingbot.smart_components.strategy_frameworks.directional_trading import DirectionalTradingBacktestingEngine
-from hummingbot.smart_components.utils.config_encoder_decoder import ConfigEncoderDecoder
+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
import os
@@ -10,14 +10,13 @@ import json
import streamlit as st
from decimal import Decimal
-from quants_lab.strategy.strategy_analysis import StrategyAnalysis
-from utils.graphs import BacktestingGraphs
-from utils.optuna_database_manager import OptunaDBManager
-from utils.os_utils import load_controllers
-from utils.st_utils import initialize_st_page
+from backend.utils.optuna_database_manager import OptunaDBManager
+from backend.utils.os_utils import load_controllers
+from frontend.visualization.graphs import BacktestingGraphs
+from frontend.visualization.strategy_analysis import StrategyAnalysis
+from frontend.st_utils import initialize_st_page
-
-initialize_st_page(title="Analyze", icon="π¬", initial_sidebar_state="collapsed")
+initialize_st_page(title="Analyze", icon="π¬")
BASE_DATA_DIR = "data/backtesting"
diff --git a/pages/backtest_create/README.md b/frontend/pages/backtesting/create/README.md
similarity index 100%
rename from pages/backtest_create/README.md
rename to frontend/pages/backtesting/create/README.md
diff --git a/frontend/pages/backtesting/create/__init__.py b/frontend/pages/backtesting/create/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/backtest_create/create.py b/frontend/pages/backtesting/create/create.py
similarity index 78%
rename from pages/backtest_create/create.py
rename to frontend/pages/backtesting/create/create.py
index 4e6322a..9008693 100644
--- a/pages/backtest_create/create.py
+++ b/frontend/pages/backtesting/create/create.py
@@ -3,15 +3,13 @@ from types import SimpleNamespace
import streamlit as st
from streamlit_elements import elements, mui
-from ui_components.dashboard import Dashboard
-from ui_components.controllers_file_explorer import ControllersFileExplorer
-from ui_components.directional_strategy_creation_card import DirectionalStrategyCreationCard
-from ui_components.editor import Editor
+from frontend.components.dashboard import Dashboard
+from frontend.components.controllers_file_explorer import ControllersFileExplorer
+from frontend.components.directional_strategy_creation_card import DirectionalStrategyCreationCard
+from frontend.components.editor import Editor
+from frontend.st_utils import initialize_st_page
-from utils.st_utils import initialize_st_page
-
-
-initialize_st_page(title="Create", icon="οΈβοΈ", initial_sidebar_state="collapsed")
+initialize_st_page(title="Create", icon="οΈβοΈ")
# TODO:
# * Add videos explaining how to the triple barrier method works and how the backtesting is designed,
diff --git a/pages/backtest_optimize/README.md b/frontend/pages/backtesting/optimize/README.md
similarity index 100%
rename from pages/backtest_optimize/README.md
rename to frontend/pages/backtesting/optimize/README.md
diff --git a/frontend/pages/backtesting/optimize/__init__.py b/frontend/pages/backtesting/optimize/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/backtest_optimize/optimize.py b/frontend/pages/backtesting/optimize/optimize.py
similarity index 79%
rename from pages/backtest_optimize/optimize.py
rename to frontend/pages/backtesting/optimize/optimize.py
index f601d75..498a9d4 100644
--- a/pages/backtest_optimize/optimize.py
+++ b/frontend/pages/backtesting/optimize/optimize.py
@@ -5,16 +5,15 @@ from types import SimpleNamespace
import streamlit as st
from streamlit_elements import elements, mui
-from ui_components.dashboard import Dashboard
-from ui_components.editor import Editor
-from ui_components.optimization_creation_card import OptimizationCreationCard
-from ui_components.optimization_run_card import OptimizationRunCard
-from ui_components.optimizations_file_explorer import OptimizationsStrategiesFileExplorer
-from 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 backend.utils import os_utils
+from frontend.st_utils import initialize_st_page
-from utils.st_utils import initialize_st_page
-
-initialize_st_page(title="Optimize", icon="π§ͺ", initial_sidebar_state="collapsed")
+initialize_st_page(title="Optimize", icon="π§ͺ")
def run_optuna_dashboard():
os_utils.execute_bash_command(f"optuna-dashboard sqlite:///data/backtesting/backtesting_report.db")
diff --git a/frontend/pages/config/__init__.py b/frontend/pages/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/bollinger_v1/README.md b/frontend/pages/config/bollinger_v1/README.md
new file mode 100644
index 0000000..0f0918a
--- /dev/null
+++ b/frontend/pages/config/bollinger_v1/README.md
@@ -0,0 +1,67 @@
+# Bollinger V1 Configuration Tool
+
+Welcome to the Bollinger V1 Configuration Tool! This tool allows you to create, modify, visualize, backtest, and save configurations for the Bollinger V1 directional trading strategy. Hereβs how you can make the most out of it.
+
+## Features
+
+- **Start from Default Configurations**: Begin with a default configuration or use the values from an existing configuration.
+- **Modify Configuration Values**: Change various parameters of the configuration to suit your trading strategy.
+- **Visualize Results**: See the impact of your changes through visual charts.
+- **Backtest Your Strategy**: Run backtests to evaluate the performance of your strategy.
+- **Save and Deploy**: Once satisfied, save the configuration to deploy it later.
+
+## How to Use
+
+### 1. Load Default Configuration
+
+Start by loading the default configuration for the Bollinger V1 strategy. This provides a baseline setup that you can customize to fit your needs.
+
+### 2. User Inputs
+
+Input various parameters for the strategy configuration. These parameters include:
+
+- **Connector Name**: Select the trading platform or exchange.
+- **Trading Pair**: Choose the cryptocurrency trading pair.
+- **Leverage**: Set the leverage ratio. (Note: if you are using spot trading, set the leverage to 1)
+- **Total Amount (Quote Currency)**: Define the total amount you want to allocate for trading.
+- **Max Executors per Side**: Specify the maximum number of executors per side.
+- **Cooldown Time**: Set the cooldown period between trades.
+- **Position Mode**: Choose between different position modes.
+- **Candles Connector**: Select the data source for candlestick data.
+- **Candles Trading Pair**: Choose the trading pair for candlestick data.
+- **Interval**: Set the interval for candlestick data.
+- **Bollinger Bands Length**: Define the length of the Bollinger Bands.
+- **Standard Deviation Multiplier**: Set the standard deviation multiplier for the Bollinger Bands.
+- **Long Threshold**: Configure the threshold for long positions.
+- **Short Threshold**: Configure the threshold for short positions.
+- **Risk Management**: Set parameters for stop loss, take profit, time limit, and trailing stop settings.
+
+### 3. Visualize Bollinger Bands
+
+Visualize the Bollinger Bands on the OHLC (Open, High, Low, Close) chart to see the impact of your configuration. Here are some hints to help you fine-tune the Bollinger Bands:
+
+- **Bollinger Bands Length**: A larger length will make the Bollinger Bands wider and smoother, while a smaller length will make them narrower and more volatile.
+- **Long Threshold**: This is a reference to the Bollinger Band. A value of 0 means the lower band, and a value of 1 means the upper band. For example, if the long threshold is 0, long positions will only be taken if the price is below the lower band.
+- **Short Threshold**: Similarly, a value of 1.1 means the price must be above the upper band by 0.1 of the bandβs range to take a short position.
+- **Thresholds**: The closer you set the thresholds to 0.5, the more trades will be executed. The farther away they are, the fewer trades will be executed.
+
+### 4. Executor Distribution
+
+The total amount in the quote currency will be distributed among the maximum number of executors per side. For example, if the total amount quote is 1000 and the max executors per side is 5, each executor will have 200 to trade. If the signal is on, the first executor will place an order and wait for the cooldown time before the next one executes, continuing this pattern for the subsequent orders.
+
+### 5. Backtesting
+
+Run backtests to evaluate the performance of your configured strategy. The backtesting section allows you to:
+
+- **Process Data**: Analyze historical trading data.
+- **Visualize Results**: See performance metrics and charts.
+- **Evaluate Accuracy**: Assess the accuracy of your strategyβs predictions and trades.
+- **Understand Close Types**: Review different types of trade closures and their frequencies.
+
+### 6. Save Configuration
+
+Once you are satisfied with your configuration and backtest results, save the configuration for future use in the Deploy tab. This allows you to deploy the same strategy later without having to reconfigure it from scratch.
+
+---
+
+Feel free to experiment with different configurations to find the optimal setup for your trading strategy. Happy trading!
\ No newline at end of file
diff --git a/frontend/pages/config/bollinger_v1/__init__.py b/frontend/pages/config/bollinger_v1/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/bollinger_v1/app.py b/frontend/pages/config/bollinger_v1/app.py
new file mode 100644
index 0000000..9f6ec6d
--- /dev/null
+++ b/frontend/pages/config/bollinger_v1/app.py
@@ -0,0 +1,71 @@
+from datetime import datetime
+
+import streamlit as st
+import pandas as pd
+import yaml
+import pandas_ta as ta # noqa: F401
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.components.backtesting import backtesting_section
+from frontend.components.config_loader import get_default_config_loader
+from frontend.components.save_config import render_save_config
+from frontend.pages.config.utils import get_max_records, get_candles
+from frontend.st_utils import initialize_st_page
+from frontend.pages.config.bollinger_v1.user_inputs import user_inputs
+from plotly.subplots import make_subplots
+
+from frontend.visualization import theme
+from frontend.visualization.backtesting import create_backtesting_figure
+from frontend.visualization.backtesting_metrics import render_backtesting_metrics, render_accuracy_metrics, \
+ render_close_types
+from frontend.visualization.candles import get_candlestick_trace
+from frontend.visualization.indicators import get_bbands_traces, get_volume_trace
+from frontend.visualization.signals import get_bollinger_v1_signal_traces
+from frontend.visualization.utils import add_traces_to_fig
+
+# Initialize the Streamlit page
+initialize_st_page(title="Bollinger V1", icon="π", initial_sidebar_state="expanded")
+backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+
+
+st.text("This tool will let you create a config for Bollinger V1 and visualize the strategy.")
+get_default_config_loader("bollinger_v1")
+
+inputs = user_inputs()
+st.session_state["default_config"] = inputs
+
+st.write("### Visualizing Bollinger Bands and Trading Signals")
+days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=3)
+# Load candle data
+candles = get_candles(connector_name=inputs["candles_connector"], trading_pair=inputs["candles_trading_pair"], interval=inputs["interval"], days=days_to_visualize)
+
+# Create a subplot with 2 rows
+fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
+ vertical_spacing=0.02, subplot_titles=('Candlestick with Bollinger Bands', 'Volume'),
+ row_heights=[0.8, 0.2])
+
+add_traces_to_fig(fig, [get_candlestick_trace(candles)], row=1, col=1)
+add_traces_to_fig(fig, get_bbands_traces(candles, inputs["bb_length"], inputs["bb_std"]), row=1, col=1)
+add_traces_to_fig(fig, get_bollinger_v1_signal_traces(candles, inputs["bb_length"], inputs["bb_std"], inputs["bb_long_threshold"], inputs["bb_short_threshold"]), row=1, col=1)
+add_traces_to_fig(fig, [get_volume_trace(candles)], row=2, col=1)
+
+fig.update_layout(**theme.get_default_layout())
+# Use Streamlit's functionality to display the plot
+st.plotly_chart(fig, use_container_width=True)
+bt_results = backtesting_section(inputs, backend_api_client)
+if bt_results:
+ fig = create_backtesting_figure(
+ df=bt_results["processed_data"],
+ executors=bt_results["executors"],
+ config=inputs)
+ c1, c2 = st.columns([0.9, 0.1])
+ with c1:
+ render_backtesting_metrics(bt_results["results"])
+ st.plotly_chart(fig, use_container_width=True)
+ with c2:
+ render_accuracy_metrics(bt_results["results"])
+ st.write("---")
+ render_close_types(bt_results["results"])
+st.write("---")
+render_save_config("bollinger_v1", inputs)
diff --git a/frontend/pages/config/bollinger_v1/user_inputs.py b/frontend/pages/config/bollinger_v1/user_inputs.py
new file mode 100644
index 0000000..8bc8988
--- /dev/null
+++ b/frontend/pages/config/bollinger_v1/user_inputs.py
@@ -0,0 +1,49 @@
+import streamlit as st
+from frontend.components.directional_trading_general_inputs import get_directional_trading_general_inputs
+from frontend.components.risk_management import get_risk_management_inputs
+
+
+def user_inputs():
+ default_config = st.session_state.get("default_config", {})
+ bb_length = default_config.get("bb_length", 100)
+ bb_std = default_config.get("bb_std", 2.0)
+ bb_long_threshold = default_config.get("bb_long_threshold", 0.0)
+ bb_short_threshold = default_config.get("bb_short_threshold", 1.0)
+ connector_name, trading_pair, leverage, total_amount_quote, max_executors_per_side, cooldown_time, position_mode, candles_connector_name, candles_trading_pair, interval = get_directional_trading_general_inputs()
+ sl, tp, time_limit, ts_ap, ts_delta, take_profit_order_type = get_risk_management_inputs()
+ with st.expander("Bollinger Bands Configuration", expanded=True):
+ c1, c2, c3, c4 = st.columns(4)
+ with c1:
+ bb_length = st.number_input("Bollinger Bands Length", min_value=5, max_value=1000, value=bb_length)
+ with c2:
+ bb_std = st.number_input("Standard Deviation Multiplier", min_value=1.0, max_value=2.0, value=bb_std)
+ with c3:
+ bb_long_threshold = st.number_input("Long Threshold", value=bb_long_threshold)
+ with c4:
+ bb_short_threshold = st.number_input("Short Threshold", value=bb_short_threshold)
+ return {
+ "controller_name": "bollinger_v1",
+ "controller_type": "directional_trading",
+ "connector_name": connector_name,
+ "trading_pair": trading_pair,
+ "leverage": leverage,
+ "total_amount_quote": total_amount_quote,
+ "max_executors_per_side": max_executors_per_side,
+ "cooldown_time": cooldown_time,
+ "position_mode": position_mode,
+ "candles_connector": candles_connector_name,
+ "candles_trading_pair": candles_trading_pair,
+ "interval": interval,
+ "bb_length": bb_length,
+ "bb_std": bb_std,
+ "bb_long_threshold": bb_long_threshold,
+ "bb_short_threshold": bb_short_threshold,
+ "stop_loss": sl,
+ "take_profit": tp,
+ "time_limit": time_limit,
+ "trailing_stop": {
+ "activation_price": ts_ap,
+ "trailing_delta": ts_delta
+ },
+ "take_profit_order_type": take_profit_order_type.value
+ }
diff --git a/frontend/pages/config/dman_maker_v2/README.md b/frontend/pages/config/dman_maker_v2/README.md
new file mode 100644
index 0000000..da98f61
--- /dev/null
+++ b/frontend/pages/config/dman_maker_v2/README.md
@@ -0,0 +1,61 @@
+# D-Man Maker V2 Configuration Tool
+
+Welcome to the D-Man Maker V2 Configuration Tool! This tool allows you to create, modify, visualize, backtest, and save configurations for the D-Man Maker V2 trading strategy. Hereβs how you can make the most out of it.
+
+## Features
+
+- **Start from Default Configurations**: Begin with a default configuration or use the values from an existing configuration.
+- **Modify Configuration Values**: Change various parameters of the configuration to suit your trading strategy.
+- **Visualize Results**: See the impact of your changes through visual charts.
+- **Backtest Your Strategy**: Run backtests to evaluate the performance of your strategy.
+- **Save and Deploy**: Once satisfied, save the configuration to deploy it later.
+
+## How to Use
+
+### 1. Load Default Configuration
+
+Start by loading the default configuration for the D-Man Maker V2 strategy. This provides a baseline setup that you can customize to fit your needs.
+
+### 2. User Inputs
+
+Input various parameters for the strategy configuration. These parameters include:
+
+- **Connector Name**: Select the trading platform or exchange.
+- **Trading Pair**: Choose the cryptocurrency trading pair.
+- **Leverage**: Set the leverage ratio. (Note: if you are using spot trading, set the leverage to 1)
+- **Total Amount (Quote Currency)**: Define the total amount you want to allocate for trading.
+- **Position Mode**: Choose between different position modes.
+- **Cooldown Time**: Set the cooldown period between trades.
+- **Executor Refresh Time**: Define how often the executors refresh.
+- **Buy/Sell Spread Distributions**: Configure the distribution of buy and sell spreads.
+- **Order Amounts**: Specify the percentages for buy and sell order amounts.
+- **Custom D-Man Maker V2 Settings**: Set specific parameters like top executor refresh time and activation bounds.
+
+### 3. Executor Distribution Visualization
+
+Visualize the distribution of your trading executors. This helps you understand how your buy and sell orders are spread across different price levels and amounts.
+
+### 4. DCA Distribution
+
+After setting the executor distribution, you will need to configure the internal distribution of the DCA (Dollar Cost Averaging). This involves multiple open orders and one close order per executor level. Visualize the DCA distribution to see how the entry prices are spread and ensure the initial DCA order amounts are above the minimum order size of the exchange.
+
+### 5. Risk Management
+
+Configure risk management settings, including take profit, stop loss, time limit, and trailing stop settings for each DCA. This step is crucial for managing your trades and minimizing risk.
+
+### 6. Backtesting
+
+Run backtests to evaluate the performance of your configured strategy. The backtesting section allows you to:
+
+- **Process Data**: Analyze historical trading data.
+- **Visualize Results**: See performance metrics and charts.
+- **Evaluate Accuracy**: Assess the accuracy of your strategyβs predictions and trades.
+- **Understand Close Types**: Review different types of trade closures and their frequencies.
+
+### 7. Save Configuration
+
+Once you are satisfied with your configuration and backtest results, save the configuration for future use in the Deploy tab. This allows you to deploy the same strategy later without having to reconfigure it from scratch.
+
+---
+
+Feel free to experiment with different configurations to find the optimal setup for your trading strategy. Happy trading!
\ No newline at end of file
diff --git a/frontend/pages/config/dman_maker_v2/__init__.py b/frontend/pages/config/dman_maker_v2/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/dman_maker_v2/app.py b/frontend/pages/config/dman_maker_v2/app.py
new file mode 100644
index 0000000..7fbd2b0
--- /dev/null
+++ b/frontend/pages/config/dman_maker_v2/app.py
@@ -0,0 +1,71 @@
+import streamlit as st
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.components.backtesting import backtesting_section
+from frontend.components.config_loader import get_default_config_loader
+from frontend.components.dca_distribution import get_dca_distribution_inputs
+from frontend.components.save_config import render_save_config
+from frontend.pages.config.dman_maker_v2.user_inputs import user_inputs
+from frontend.st_utils import initialize_st_page
+from frontend.visualization.backtesting import create_backtesting_figure
+from frontend.visualization.backtesting_metrics import render_backtesting_metrics, render_accuracy_metrics, \
+ render_close_types
+from frontend.visualization.dca_builder import create_dca_graph
+from frontend.visualization.executors_distribution import create_executors_distribution_traces
+
+# Initialize the Streamlit page
+initialize_st_page(title="D-Man Maker V2", icon="π§ββοΈ")
+backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+
+
+# Page content
+st.text("This tool will let you create a config for D-Man Maker V2 and upload it to the BackendAPI.")
+get_default_config_loader("dman_maker_v2")
+
+inputs = user_inputs()
+with st.expander("Executor Distribution:", expanded=True):
+ fig = create_executors_distribution_traces(inputs["buy_spreads"], inputs["sell_spreads"], inputs["buy_amounts_pct"], inputs["sell_amounts_pct"], inputs["total_amount_quote"])
+ st.plotly_chart(fig, use_container_width=True)
+
+dca_inputs = get_dca_distribution_inputs()
+
+st.write("### Visualizing DCA Distribution for specific Executor Level")
+st.write("---")
+buy_order_levels = len(inputs["buy_spreads"])
+sell_order_levels = len(inputs["sell_spreads"])
+
+buy_executor_levels = [f"BUY_{i}" for i in range(buy_order_levels)]
+sell_executor_levels = [f"SELL_{i}" for i in range(sell_order_levels)]
+c1, c2 = st.columns(2)
+with c1:
+ executor_level = st.selectbox("Executor Level", buy_executor_levels + sell_executor_levels)
+ side, level = executor_level.split("_")
+ if side == "BUY":
+ dca_amount = inputs["buy_amounts_pct"][int(level)] * inputs["total_amount_quote"]
+ else:
+ dca_amount = inputs["sell_amounts_pct"][int(level)] * inputs["total_amount_quote"]
+with c2:
+ st.metric(label="DCA Amount", value=f"{dca_amount:.2f}")
+fig = create_dca_graph(dca_inputs, dca_amount)
+st.plotly_chart(fig, use_container_width=True)
+
+# Combine inputs and dca_inputs into final config
+config = {**inputs, **dca_inputs}
+st.session_state["default_config"] = config
+bt_results = backtesting_section(config, backend_api_client)
+if bt_results:
+ fig = create_backtesting_figure(
+ df=bt_results["processed_data"],
+ executors=bt_results["executors"],
+ config=inputs)
+ c1, c2 = st.columns([0.9, 0.1])
+ with c1:
+ render_backtesting_metrics(bt_results["results"])
+ st.plotly_chart(fig, use_container_width=True)
+ with c2:
+ render_accuracy_metrics(bt_results["results"])
+ st.write("---")
+ render_close_types(bt_results["results"])
+st.write("---")
+render_save_config("dman_maker_v2", config)
\ No newline at end of file
diff --git a/frontend/pages/config/dman_maker_v2/user_inputs.py b/frontend/pages/config/dman_maker_v2/user_inputs.py
new file mode 100644
index 0000000..5ccf4d9
--- /dev/null
+++ b/frontend/pages/config/dman_maker_v2/user_inputs.py
@@ -0,0 +1,37 @@
+import streamlit as st
+
+from frontend.components.executors_distribution import get_executors_distribution_inputs
+from frontend.components.market_making_general_inputs import get_market_making_general_inputs
+
+
+def user_inputs():
+ connector_name, trading_pair, leverage, total_amount_quote, position_mode, cooldown_time, executor_refresh_time, _, _, _ = get_market_making_general_inputs()
+ buy_spread_distributions, sell_spread_distributions, buy_order_amounts_pct, sell_order_amounts_pct = get_executors_distribution_inputs()
+ with st.expander("Custom D-Man Maker V2 Settings"):
+ c1, c2 = st.columns(2)
+ with c1:
+ top_executor_refresh_time = st.number_input("Top Refresh Time (minutes)", value=60) * 60
+ with c2:
+ executor_activation_bounds = st.number_input("Activation Bounds (%)", value=0.1) / 100
+ # Create the config
+ config = {
+ "controller_name": "dman_maker_v2",
+ "controller_type": "market_making",
+ "manual_kill_switch": None,
+ "candles_config": [],
+ "connector_name": connector_name,
+ "trading_pair": trading_pair,
+ "total_amount_quote": total_amount_quote,
+ "buy_spreads": buy_spread_distributions,
+ "sell_spreads": sell_spread_distributions,
+ "buy_amounts_pct": buy_order_amounts_pct,
+ "sell_amounts_pct": sell_order_amounts_pct,
+ "executor_refresh_time": executor_refresh_time,
+ "cooldown_time": cooldown_time,
+ "leverage": leverage,
+ "position_mode": position_mode,
+ "top_executor_refresh_time": top_executor_refresh_time,
+ "executor_activation_bounds": [executor_activation_bounds]
+ }
+
+ return config
diff --git a/frontend/pages/config/dman_v5/README.md b/frontend/pages/config/dman_v5/README.md
new file mode 100644
index 0000000..2fa8d53
--- /dev/null
+++ b/frontend/pages/config/dman_v5/README.md
@@ -0,0 +1,19 @@
+# D-Man Maker V2
+
+## Features
+- **Interactive Configuration**: Configure market making parameters such as spreads, amounts, and order levels through an intuitive web interface.
+- **Visual Feedback**: Visualize order spread and amount distributions using dynamic Plotly charts.
+- **Backend Integration**: Save and deploy configurations directly to a backend system for active management and execution.
+
+### Using the Tool
+1. **Configure Parameters**: Use the Streamlit interface to input parameters such as connector type, trading pair, and leverage.
+2. **Set Distributions**: Define distributions for buy and sell orders, including spread and amount, either manually or through predefined distribution types like Geometric or Fibonacci.
+3. **Visualize Orders**: View the configured order distributions on a Plotly graph, which illustrates the relationship between spread and amount.
+4. **Export Configuration**: Once the configuration is set, export it as a YAML file or directly upload it to the Backend API.
+5. **Upload**: Use the "Upload Config to BackendAPI" button to send your configuration to the backend system. Then can be used to deploy a new bot.
+
+## Troubleshooting
+- **UI Not Loading**: Ensure all Python dependencies are installed and that the Streamlit server is running correctly.
+- **API Errors**: Check the console for any error messages that may indicate issues with the backend connection.
+
+For more detailed documentation on the backend API and additional configurations, please refer to the project's documentation or contact the development team.
\ No newline at end of file
diff --git a/frontend/pages/config/dman_v5/__init__.py b/frontend/pages/config/dman_v5/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/dman_v5/app.py b/frontend/pages/config/dman_v5/app.py
new file mode 100644
index 0000000..ac39d9f
--- /dev/null
+++ b/frontend/pages/config/dman_v5/app.py
@@ -0,0 +1,147 @@
+import streamlit as st
+import pandas as pd
+import plotly.graph_objects as go
+import yaml
+from plotly.subplots import make_subplots
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.st_utils import initialize_st_page
+
+# Initialize the Streamlit page
+initialize_st_page(title="D-Man V5", icon="π", initial_sidebar_state="expanded")
+
+@st.cache_data
+def get_candles(connector_name, trading_pair, interval, max_records):
+ backend_client = BackendAPIClient(BACKEND_API_HOST, BACKEND_API_PORT)
+ return backend_client.get_real_time_candles(connector_name, trading_pair, interval, max_records)
+
+@st.cache_data
+def add_indicators(df, macd_fast, macd_slow, macd_signal, diff_lookback):
+ # MACD
+ df.ta.macd(fast=macd_fast, slow=macd_slow, signal=macd_signal, append=True)
+
+ # Decision Logic
+ macdh = df[f"MACDh_{macd_fast}_{macd_slow}_{macd_signal}"]
+ macdh_diff = df[f"MACDh_{macd_fast}_{macd_slow}_{macd_signal}"].diff(diff_lookback)
+
+ long_condition = (macdh > 0) & (macdh_diff > 0)
+ short_condition = (macdh < 0) & (macdh_diff < 0)
+
+ df["signal"] = 0
+ df.loc[long_condition, "signal"] = 1
+ df.loc[short_condition, "signal"] = -1
+
+ return df
+
+st.write("## Configuration")
+c1, c2, c3 = st.columns(3)
+with c1:
+ connector_name = st.text_input("Connector Name", value="binance_perpetual")
+ trading_pair = st.text_input("Trading Pair", value="WLD-USDT")
+with c2:
+ interval = st.selectbox("Candle Interval", ["1m", "3m", "5m", "15m", "30m"], index=1)
+ max_records = st.number_input("Max Records", min_value=100, max_value=10000, value=1000)
+with c3:
+ macd_fast = st.number_input("MACD Fast", min_value=1, value=21)
+ macd_slow = st.number_input("MACD Slow", min_value=1, value=42)
+ macd_signal = st.number_input("MACD Signal", min_value=1, value=9)
+ diff_lookback = st.number_input("MACD Diff Lookback", min_value=1, value=5)
+
+# Fetch and process data
+candle_data = get_candles(connector_name, trading_pair, interval, max_records)
+df = pd.DataFrame(candle_data)
+df.index = pd.to_datetime(df['timestamp'], unit='s')
+df = add_indicators(df, macd_fast, macd_slow, macd_signal, diff_lookback)
+
+# Prepare data for signals
+signals = df[df['signal'] != 0]
+buy_signals = signals[signals['signal'] == 1]
+sell_signals = signals[signals['signal'] == -1]
+
+
+# Define your color palette
+tech_colors = {
+ 'upper_band': '#4682B4',
+ 'middle_band': '#FFD700',
+ 'lower_band': '#32CD32',
+ 'buy_signal': '#1E90FF',
+ 'sell_signal': '#FF0000',
+}
+
+# Create a subplot with 3 rows
+fig = make_subplots(rows=3, cols=1, shared_xaxes=True,
+ vertical_spacing=0.05, # Adjust spacing to make the plot look better
+ subplot_titles=('Candlestick', 'MACD Line and Histogram', 'Trading Signals'),
+ row_heights=[0.5, 0.3, 0.2]) # Adjust heights to give more space to candlestick and MACD
+
+# Candlestick and Bollinger Bands
+fig.add_trace(go.Candlestick(x=df.index,
+ open=df['open'],
+ high=df['high'],
+ low=df['low'],
+ close=df['close'],
+ name="Candlesticks", increasing_line_color='#2ECC71', decreasing_line_color='#E74C3C'),
+ row=1, col=1)
+
+# MACD Line and Histogram
+fig.add_trace(go.Scatter(x=df.index, y=df[f"MACD_{macd_fast}_{macd_slow}_{macd_signal}"], line=dict(color='orange'), name='MACD Line'), row=2, col=1)
+fig.add_trace(go.Scatter(x=df.index, y=df[f"MACDs_{macd_fast}_{macd_slow}_{macd_signal}"], line=dict(color='purple'), name='MACD Signal'), row=2, col=1)
+fig.add_trace(go.Bar(x=df.index, y=df[f"MACDh_{macd_fast}_{macd_slow}_{macd_signal}"], name='MACD Histogram', marker_color=df[f"MACDh_{macd_fast}_{macd_slow}_{macd_signal}"].apply(lambda x: '#FF6347' if x < 0 else '#32CD32')), row=2, col=1)
+# Signals plot
+fig.add_trace(go.Scatter(x=buy_signals.index, y=buy_signals['close'], mode='markers',
+ marker=dict(color=tech_colors['buy_signal'], size=10, symbol='triangle-up'),
+ name='Buy Signal'), row=1, col=1)
+fig.add_trace(go.Scatter(x=sell_signals.index, y=sell_signals['close'], mode='markers',
+ marker=dict(color=tech_colors['sell_signal'], size=10, symbol='triangle-down'),
+ name='Sell Signal'), row=1, col=1)
+
+# Trading Signals
+fig.add_trace(go.Scatter(x=signals.index, y=signals['signal'], mode='markers', marker=dict(color=signals['signal'].map({1: '#1E90FF', -1: '#FF0000'}), size=10), name='Trading Signals'), row=3, col=1)
+
+# Update layout settings for a clean look
+fig.update_layout(height=1000, title="MACD and Bollinger Bands Strategy", xaxis_title="Time", yaxis_title="Price", template="plotly_dark", showlegend=True)
+fig.update_xaxes(rangeslider_visible=False, row=1, col=1)
+fig.update_xaxes(rangeslider_visible=False, row=2, col=1)
+fig.update_xaxes(rangeslider_visible=False, row=3, col=1)
+
+# Display the chart
+st.plotly_chart(fig, use_container_width=True)
+
+
+c1, c2, c3 = st.columns([2, 2, 1])
+
+with c1:
+ config_base = st.text_input("Config Base", value=f"macd_bb_v1-{connector_name}-{trading_pair.split('-')[0]}")
+with c2:
+ config_tag = st.text_input("Config Tag", value="1.1")
+
+# Save the configuration
+id = f"{config_base}-{config_tag}"
+
+config = {
+ "id": id,
+ "connector_name": connector_name,
+ "trading_pair": trading_pair,
+ "interval": interval,
+ "macd_fast": macd_fast,
+ "macd_slow": macd_slow,
+ "macd_signal": macd_signal,
+}
+
+yaml_config = yaml.dump(config, default_flow_style=False)
+
+with c3:
+ download_config = st.download_button(
+ label="Download YAML",
+ data=yaml_config,
+ file_name=f'{id.lower()}.yml',
+ mime='text/yaml'
+ )
+ upload_config_to_backend = st.button("Upload Config to BackendAPI")
+
+
+if upload_config_to_backend:
+ backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+ backend_api_client.add_controller_config(config)
+ st.success("Config uploaded successfully!")
diff --git a/frontend/pages/config/kalman_filter_v1/README.md b/frontend/pages/config/kalman_filter_v1/README.md
new file mode 100644
index 0000000..2fa8d53
--- /dev/null
+++ b/frontend/pages/config/kalman_filter_v1/README.md
@@ -0,0 +1,19 @@
+# D-Man Maker V2
+
+## Features
+- **Interactive Configuration**: Configure market making parameters such as spreads, amounts, and order levels through an intuitive web interface.
+- **Visual Feedback**: Visualize order spread and amount distributions using dynamic Plotly charts.
+- **Backend Integration**: Save and deploy configurations directly to a backend system for active management and execution.
+
+### Using the Tool
+1. **Configure Parameters**: Use the Streamlit interface to input parameters such as connector type, trading pair, and leverage.
+2. **Set Distributions**: Define distributions for buy and sell orders, including spread and amount, either manually or through predefined distribution types like Geometric or Fibonacci.
+3. **Visualize Orders**: View the configured order distributions on a Plotly graph, which illustrates the relationship between spread and amount.
+4. **Export Configuration**: Once the configuration is set, export it as a YAML file or directly upload it to the Backend API.
+5. **Upload**: Use the "Upload Config to BackendAPI" button to send your configuration to the backend system. Then can be used to deploy a new bot.
+
+## Troubleshooting
+- **UI Not Loading**: Ensure all Python dependencies are installed and that the Streamlit server is running correctly.
+- **API Errors**: Check the console for any error messages that may indicate issues with the backend connection.
+
+For more detailed documentation on the backend API and additional configurations, please refer to the project's documentation or contact the development team.
\ No newline at end of file
diff --git a/frontend/pages/config/kalman_filter_v1/__init__.py b/frontend/pages/config/kalman_filter_v1/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/kalman_filter_v1/app.py b/frontend/pages/config/kalman_filter_v1/app.py
new file mode 100644
index 0000000..5f7e358
--- /dev/null
+++ b/frontend/pages/config/kalman_filter_v1/app.py
@@ -0,0 +1,225 @@
+import streamlit as st
+import pandas as pd
+import plotly.graph_objects as go
+import yaml
+from hummingbot.connector.connector_base import OrderType
+from pykalman import KalmanFilter
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.st_utils import initialize_st_page
+
+# Initialize the Streamlit page
+initialize_st_page(title="Kalman Filter V1", icon="π", initial_sidebar_state="expanded")
+
+
+@st.cache_data
+def get_candles(connector_name="binance", trading_pair="BTC-USDT", interval="1m", max_records=5000):
+ backend_client = BackendAPIClient(BACKEND_API_HOST, BACKEND_API_PORT)
+ return backend_client.get_real_time_candles(connector_name, trading_pair, interval, max_records)
+
+@st.cache_data
+def add_indicators(df, observation_covariance=1, transition_covariance=0.01, initial_state_covariance=0.001):
+ # Add Bollinger Bands
+ # Construct a Kalman filter
+ kf = KalmanFilter(transition_matrices=[1],
+ observation_matrices=[1],
+ initial_state_mean=df["close"].values[0],
+ initial_state_covariance=initial_state_covariance,
+ observation_covariance=observation_covariance,
+ transition_covariance=transition_covariance)
+ mean, cov = kf.filter(df["close"].values)
+ df["kf"] = pd.Series(mean.flatten(), index=df["close"].index)
+ df["kf_upper"] = pd.Series(mean.flatten() + 1.96 * cov.flatten(), index=df["close"].index)
+ df["kf_lower"] = pd.Series(mean.flatten() - 1.96 * cov.flatten(), index=df["close"].index)
+
+ # Generate signal
+ long_condition = df["close"] < df["kf_lower"]
+ short_condition = df["close"] > df["kf_upper"]
+
+ # Generate signal
+ df["signal"] = 0
+ df.loc[long_condition, "signal"] = 1
+ df.loc[short_condition, "signal"] = -1
+ return df
+
+
+st.text("This tool will let you create a config for Kalman Filter V1 and visualize the strategy.")
+st.write("---")
+
+# Inputs for Kalman Filter configuration
+st.write("## Candles Configuration")
+c1, c2, c3, c4 = st.columns(4)
+with c1:
+ connector_name = st.text_input("Connector Name", value="binance_perpetual")
+ candles_connector = st.text_input("Candles Connector", value="binance_perpetual")
+with c2:
+ trading_pair = st.text_input("Trading Pair", value="WLD-USDT")
+ candles_trading_pair = st.text_input("Candles Trading Pair", value="WLD-USDT")
+with c3:
+ interval = st.selectbox("Candle Interval", options=["1m", "3m", "5m", "15m", "30m"], index=1)
+with c4:
+ max_records = st.number_input("Max Records", min_value=100, max_value=10000, value=1000)
+
+
+st.write("## Positions Configuration")
+c1, c2, c3, c4 = st.columns(4)
+with c1:
+ sl = st.number_input("Stop Loss (%)", min_value=0.0, max_value=100.0, value=2.0, step=0.1)
+ tp = st.number_input("Take Profit (%)", min_value=0.0, max_value=100.0, value=3.0, step=0.1)
+ take_profit_order_type = st.selectbox("Take Profit Order Type", (OrderType.LIMIT, OrderType.MARKET))
+with c2:
+ ts_ap = st.number_input("Trailing Stop Activation Price (%)", min_value=0.0, max_value=100.0, value=1.0, step=0.1)
+ ts_delta = st.number_input("Trailing Stop Delta (%)", min_value=0.0, max_value=100.0, value=0.3, step=0.1)
+ time_limit = st.number_input("Time Limit (minutes)", min_value=0, value=60 * 6)
+with c3:
+ executor_amount_quote = st.number_input("Executor Amount Quote", min_value=10.0, value=100.0, step=1.0)
+ max_executors_per_side = st.number_input("Max Executors Per Side", min_value=1, value=2)
+ cooldown_time = st.number_input("Cooldown Time (seconds)", min_value=0, value=300)
+with c4:
+ leverage = st.number_input("Leverage", min_value=1, value=20)
+ position_mode = st.selectbox("Position Mode", ("HEDGE", "ONEWAY"))
+
+st.write("## Kalman Filter Configuration")
+c1, c2 = st.columns(2)
+with c1:
+ observation_covariance = st.number_input("Observation Covariance", value=1.0)
+with c2:
+ transition_covariance = st.number_input("Transition Covariance", value=0.001, step=0.0001, format="%.4f")
+
+
+# Load candle data
+candle_data = get_candles(connector_name=candles_connector, trading_pair=candles_trading_pair, interval=interval, max_records=max_records)
+df = pd.DataFrame(candle_data)
+df.index = pd.to_datetime(df['timestamp'], unit='s')
+candles_processed = add_indicators(df, observation_covariance, transition_covariance)
+
+
+
+# Prepare data for signals
+signals = candles_processed[candles_processed['signal'] != 0]
+buy_signals = signals[signals['signal'] == 1]
+sell_signals = signals[signals['signal'] == -1]
+
+from plotly.subplots import make_subplots
+
+# Define your color palette
+tech_colors = {
+ 'upper_band': '#4682B4', # Steel Blue for the Upper Bollinger Band
+ 'middle_band': '#FFD700', # Gold for the Middle Bollinger Band
+ 'lower_band': '#32CD32', # Green for the Lower Bollinger Band
+ 'buy_signal': '#1E90FF', # Dodger Blue for Buy Signals
+ 'sell_signal': '#FF0000', # Red for Sell Signals
+}
+
+# Create a subplot with 2 rows
+fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
+ vertical_spacing=0.02, subplot_titles=('Candlestick with Kalman Filter', 'Trading Signals'),
+ row_heights=[0.7, 0.3])
+
+# Candlestick plot
+fig.add_trace(go.Candlestick(x=candles_processed.index,
+ open=candles_processed['open'],
+ high=candles_processed['high'],
+ low=candles_processed['low'],
+ close=candles_processed['close'],
+ name="Candlesticks", increasing_line_color='#2ECC71', decreasing_line_color='#E74C3C'),
+ row=1, col=1)
+
+# Bollinger Bands
+fig.add_trace(go.Scatter(x=candles_processed.index, y=candles_processed['kf_upper'], line=dict(color=tech_colors['upper_band']), name='Upper Band'), row=1, col=1)
+fig.add_trace(go.Scatter(x=candles_processed.index, y=candles_processed['kf'], line=dict(color=tech_colors['middle_band']), name='Middle Band'), row=1, col=1)
+fig.add_trace(go.Scatter(x=candles_processed.index, y=candles_processed['kf_lower'], line=dict(color=tech_colors['lower_band']), name='Lower Band'), row=1, col=1)
+
+# Signals plot
+fig.add_trace(go.Scatter(x=buy_signals.index, y=buy_signals['close'], mode='markers',
+ marker=dict(color=tech_colors['buy_signal'], size=10, symbol='triangle-up'),
+ name='Buy Signal'), row=1, col=1)
+fig.add_trace(go.Scatter(x=sell_signals.index, y=sell_signals['close'], mode='markers',
+ marker=dict(color=tech_colors['sell_signal'], size=10, symbol='triangle-down'),
+ name='Sell Signal'), row=1, col=1)
+
+fig.add_trace(go.Scatter(x=signals.index, y=signals['signal'], mode='markers',
+ marker=dict(color=signals['signal'].map({1: tech_colors['buy_signal'], -1: tech_colors['sell_signal']}), size=10),
+ showlegend=False), row=2, col=1)
+
+# Update layout
+fig.update_layout(
+ height=1000, # Increased height for better visibility
+ title="Kalman Filter and Trading Signals",
+ xaxis_title="Time",
+ yaxis_title="Price",
+ template="plotly_dark",
+ showlegend=False
+)
+
+# Update xaxis properties
+fig.update_xaxes(
+ rangeslider_visible=False, # Disable range slider for all
+ row=1, col=1
+)
+fig.update_xaxes(
+ row=2, col=1
+)
+
+# Update yaxis properties
+fig.update_yaxes(
+ title_text="Price", row=1, col=1
+)
+fig.update_yaxes(
+ title_text="Signal", row=2, col=1
+)
+
+# Use Streamlit's functionality to display the plot
+st.plotly_chart(fig, use_container_width=True)
+
+c1, c2, c3 = st.columns([2, 2, 1])
+
+with c1:
+ config_base = st.text_input("Config Base", value=f"bollinger_v1-{connector_name}-{trading_pair.split('-')[0]}")
+with c2:
+ config_tag = st.text_input("Config Tag", value="1.1")
+
+id = f"{config_base}-{config_tag}"
+config = {
+ "id": id,
+ "controller_name": "bollinger_v1",
+ "controller_type": "directional_trading",
+ "manual_kill_switch": None,
+ "candles_config": [],
+ "connector_name": connector_name,
+ "trading_pair": trading_pair,
+ "executor_amount_quote": executor_amount_quote,
+ "max_executors_per_side": max_executors_per_side,
+ "cooldown_time": cooldown_time,
+ "leverage": leverage,
+ "position_mode": position_mode,
+ "stop_loss": sl / 100,
+ "take_profit": tp / 100,
+ "time_limit": time_limit,
+ "take_profit_order_type": take_profit_order_type.value,
+ "trailing_stop": {
+ "activation_price": ts_ap / 100,
+ "trailing_delta": ts_delta / 100
+ },
+ "candles_connector": candles_connector,
+ "candles_trading_pair": candles_trading_pair,
+ "interval": interval,
+}
+
+yaml_config = yaml.dump(config, default_flow_style=False)
+
+with c3:
+ download_config = st.download_button(
+ label="Download YAML",
+ data=yaml_config,
+ file_name=f'{id.lower()}.yml',
+ mime='text/yaml'
+ )
+ upload_config_to_backend = st.button("Upload Config to BackendAPI")
+
+
+if upload_config_to_backend:
+ backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+ backend_api_client.add_controller_config(config)
+ st.success("Config uploaded successfully!")
\ No newline at end of file
diff --git a/frontend/pages/config/macd_bb_v1/README.md b/frontend/pages/config/macd_bb_v1/README.md
new file mode 100644
index 0000000..b7e7adb
--- /dev/null
+++ b/frontend/pages/config/macd_bb_v1/README.md
@@ -0,0 +1,80 @@
+# MACD BB V1 Configuration Tool
+
+Welcome to the MACD BB V1 Configuration Tool! This tool allows you to create, modify, visualize, backtest, and save configurations for the MACD BB V1 directional trading strategy. Hereβs how you can make the most out of it.
+
+## Features
+
+- **Start from Default Configurations**: Begin with a default configuration or use the values from an existing configuration.
+- **Modify Configuration Values**: Change various parameters of the configuration to suit your trading strategy.
+- **Visualize Results**: See the impact of your changes through visual charts.
+- **Backtest Your Strategy**: Run backtests to evaluate the performance of your strategy.
+- **Save and Deploy**: Once satisfied, save the configuration to deploy it later.
+
+## How to Use
+
+### 1. Load Default Configuration
+
+Start by loading the default configuration for the MACD BB V1 strategy. This provides a baseline setup that you can customize to fit your needs.
+
+### 2. User Inputs
+
+Input various parameters for the strategy configuration. These parameters include:
+
+- **Connector Name**: Select the trading platform or exchange.
+- **Trading Pair**: Choose the cryptocurrency trading pair.
+- **Leverage**: Set the leverage ratio. (Note: if you are using spot trading, set the leverage to 1)
+- **Total Amount (Quote Currency)**: Define the total amount you want to allocate for trading.
+- **Max Executors per Side**: Specify the maximum number of executors per side.
+- **Cooldown Time**: Set the cooldown period between trades.
+- **Position Mode**: Choose between different position modes.
+- **Candles Connector**: Select the data source for candlestick data.
+- **Candles Trading Pair**: Choose the trading pair for candlestick data.
+- **Interval**: Set the interval for candlestick data.
+- **Bollinger Bands Length**: Define the length of the Bollinger Bands.
+- **Standard Deviation Multiplier**: Set the standard deviation multiplier for the Bollinger Bands.
+- **Long Threshold**: Configure the threshold for long positions.
+- **Short Threshold**: Configure the threshold for short positions.
+- **MACD Fast**: Set the fast period for the MACD indicator.
+- **MACD Slow**: Set the slow period for the MACD indicator.
+- **MACD Signal**: Set the signal period for the MACD indicator.
+- **Risk Management**: Set parameters for stop loss, take profit, time limit, and trailing stop settings.
+
+### 3. Visualize Indicators
+
+Visualize the Bollinger Bands and MACD on the OHLC (Open, High, Low, Close) chart to see the impact of your configuration. Here are some hints to help you fine-tune the indicators:
+
+- **Bollinger Bands Length**: A larger length will make the Bollinger Bands wider and smoother, while a smaller length will make them narrower and more volatile.
+- **Long Threshold**: This is a reference to the Bollinger Band. A value of 0 means the lower band, and a value of 1 means the upper band. For example, if the long threshold is 0, long positions will only be taken if the price is below the lower band.
+- **Short Threshold**: Similarly, a value of 1.1 means the price must be above the upper band by 0.1 of the bandβs range to take a short position.
+- **Thresholds**: The closer you set the thresholds to 0.5, the more trades will be executed. The farther away they are, the fewer trades will be executed.
+- **MACD**: The MACD is used to determine trend changes. If the MACD value is negative and the histogram becomes positive, it signals a market trend up, suggesting a long position. Conversely, if the MACD value is positive and the histogram becomes negative, it signals a market trend down, suggesting a short position.
+
+### Combining MACD and Bollinger Bands for Trade Signals
+
+The MACD BB V1 strategy uses the MACD to identify potential trend changes and the Bollinger Bands to filter these signals:
+
+- **Long Signal**: The MACD value must be negative, and the histogram must become positive, indicating a potential uptrend. The price must also be below the long threshold of the Bollinger Bands (e.g., below the lower band if the threshold is 0).
+- **Short Signal**: The MACD value must be positive, and the histogram must become negative, indicating a potential downtrend. The price must also be above the short threshold of the Bollinger Bands (e.g., above the upper band if the threshold is 1.1).
+
+This combination ensures that you only take trend-following trades when the market is already deviated from the mean, enhancing the effectiveness of your trading strategy.
+
+### 4. Executor Distribution
+
+The total amount in the quote currency will be distributed among the maximum number of executors per side. For example, if the total amount quote is 1000 and the max executors per side is 5, each executor will have 200 to trade. If the signal is on, the first executor will place an order and wait for the cooldown time before the next one executes, continuing this pattern for the subsequent orders.
+
+### 5. Backtesting
+
+Run backtests to evaluate the performance of your configured strategy. The backtesting section allows you to:
+
+- **Process Data**: Analyze historical trading data.
+- **Visualize Results**: See performance metrics and charts.
+- **Evaluate Accuracy**: Assess the accuracy of your strategyβs predictions and trades.
+- **Understand Close Types**: Review different types of trade closures and their frequencies.
+
+### 6. Save Configuration
+
+Once you are satisfied with your configuration and backtest results, save the configuration for future use in the Deploy tab. This allows you to deploy the same strategy later without having to reconfigure it from scratch.
+
+---
+
+Feel free to experiment with different configurations to find the optimal setup for your trading strategy. Happy trading!
\ No newline at end of file
diff --git a/frontend/pages/config/macd_bb_v1/__init__.py b/frontend/pages/config/macd_bb_v1/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/macd_bb_v1/app.py b/frontend/pages/config/macd_bb_v1/app.py
new file mode 100644
index 0000000..f061fcc
--- /dev/null
+++ b/frontend/pages/config/macd_bb_v1/app.py
@@ -0,0 +1,67 @@
+import streamlit as st
+import pandas as pd
+import plotly.graph_objects as go
+import yaml
+from plotly.subplots import make_subplots
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.components.backtesting import backtesting_section
+from frontend.components.config_loader import get_default_config_loader
+from frontend.components.save_config import render_save_config
+from frontend.pages.config.macd_bb_v1.user_inputs import user_inputs
+from frontend.pages.config.utils import get_candles, get_max_records
+from frontend.st_utils import initialize_st_page
+from frontend.visualization import theme
+from frontend.visualization.backtesting import create_backtesting_figure
+from frontend.visualization.backtesting_metrics import render_backtesting_metrics, render_accuracy_metrics, \
+ render_close_types
+from frontend.visualization.candles import get_candlestick_trace
+from frontend.visualization.indicators import get_bbands_traces, get_volume_trace, get_macd_traces
+from frontend.visualization.signals import get_macdbb_v1_signal_traces
+from frontend.visualization.utils import add_traces_to_fig
+
+# Initialize the Streamlit page
+initialize_st_page(title="MACD_BB V1", icon="π", initial_sidebar_state="expanded")
+backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+
+get_default_config_loader("macd_bb_v1")
+# User inputs
+inputs = user_inputs()
+st.session_state["default_config"] = inputs
+
+st.write("### Visualizing MACD Bollinger Trading Signals")
+days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=3)
+# Load candle data
+candles = get_candles(connector_name=inputs["candles_connector"], trading_pair=inputs["candles_trading_pair"], interval=inputs["interval"], days=days_to_visualize)
+
+# Create a subplot with 2 rows
+fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
+ vertical_spacing=0.02, subplot_titles=('Candlestick with Bollinger Bands', 'Volume', "MACD"),
+ row_heights=[0.8, 0.2])
+add_traces_to_fig(fig, [get_candlestick_trace(candles)], row=1, col=1)
+add_traces_to_fig(fig, get_bbands_traces(candles, inputs["bb_length"], inputs["bb_std"]), row=1, col=1)
+add_traces_to_fig(fig, get_macdbb_v1_signal_traces(df=candles, bb_length=inputs["bb_length"], bb_std=inputs["bb_std"],
+ bb_long_threshold=inputs["bb_long_threshold"], bb_short_threshold=inputs["bb_short_threshold"],
+ macd_fast=inputs["macd_fast"], macd_slow=inputs["macd_slow"], macd_signal=inputs["macd_signal"]), row=1, col=1)
+add_traces_to_fig(fig, get_macd_traces(df=candles, macd_fast=inputs["macd_fast"], macd_slow=inputs["macd_slow"], macd_signal=inputs["macd_signal"]), row=2, col=1)
+
+fig.update_layout(**theme.get_default_layout())
+# Use Streamlit's functionality to display the plot
+st.plotly_chart(fig, use_container_width=True)
+bt_results = backtesting_section(inputs, backend_api_client)
+if bt_results:
+ fig = create_backtesting_figure(
+ df=bt_results["processed_data"],
+ executors=bt_results["executors"],
+ config=inputs)
+ c1, c2 = st.columns([0.9, 0.1])
+ with c1:
+ render_backtesting_metrics(bt_results["results"])
+ st.plotly_chart(fig, use_container_width=True)
+ with c2:
+ render_accuracy_metrics(bt_results["results"])
+ st.write("---")
+ render_close_types(bt_results["results"])
+st.write("---")
+render_save_config("bollinger_v1", inputs)
diff --git a/frontend/pages/config/macd_bb_v1/user_inputs.py b/frontend/pages/config/macd_bb_v1/user_inputs.py
new file mode 100644
index 0000000..3e7e212
--- /dev/null
+++ b/frontend/pages/config/macd_bb_v1/user_inputs.py
@@ -0,0 +1,62 @@
+import streamlit as st
+from frontend.components.directional_trading_general_inputs import get_directional_trading_general_inputs
+from frontend.components.risk_management import get_risk_management_inputs
+
+
+def user_inputs():
+ default_config = st.session_state.get("default_config", {})
+ bb_length = default_config.get("bb_length", 100)
+ bb_std = default_config.get("bb_std", 2.0)
+ bb_long_threshold = default_config.get("bb_long_threshold", 0.0)
+ bb_short_threshold = default_config.get("bb_short_threshold", 1.0)
+ macd_fast = default_config.get("macd_fast", 21)
+ macd_slow = default_config.get("macd_slow", 42)
+ macd_signal = default_config.get("macd_signal", 9)
+ connector_name, trading_pair, leverage, total_amount_quote, max_executors_per_side, cooldown_time, position_mode, candles_connector_name, candles_trading_pair, interval = get_directional_trading_general_inputs()
+ sl, tp, time_limit, ts_ap, ts_delta, take_profit_order_type = get_risk_management_inputs()
+ with st.expander("MACD Bollinger Configuration", expanded=True):
+ c1, c2, c3, c4, c5, c6, c7 = st.columns(7)
+ with c1:
+ bb_length = st.number_input("Bollinger Bands Length", min_value=5, max_value=1000, value=bb_length)
+ with c2:
+ bb_std = st.number_input("Standard Deviation Multiplier", min_value=1.0, max_value=2.0, value=bb_std)
+ with c3:
+ bb_long_threshold = st.number_input("Long Threshold", value=bb_long_threshold)
+ with c4:
+ bb_short_threshold = st.number_input("Short Threshold", value=bb_short_threshold)
+ with c5:
+ macd_fast = st.number_input("MACD Fast", min_value=1, value=macd_fast)
+ with c6:
+ macd_slow = st.number_input("MACD Slow", min_value=1, value=macd_slow)
+ with c7:
+ macd_signal = st.number_input("MACD Signal", min_value=1, value=macd_signal)
+
+ return {
+ "controller_name": "macd_bb_v1",
+ "controller_type": "directional_trading",
+ "connector_name": connector_name,
+ "trading_pair": trading_pair,
+ "leverage": leverage,
+ "total_amount_quote": total_amount_quote,
+ "max_executors_per_side": max_executors_per_side,
+ "cooldown_time": cooldown_time,
+ "position_mode": position_mode,
+ "candles_connector": candles_connector_name,
+ "candles_trading_pair": candles_trading_pair,
+ "interval": interval,
+ "bb_length": bb_length,
+ "bb_std": bb_std,
+ "bb_long_threshold": bb_long_threshold,
+ "bb_short_threshold": bb_short_threshold,
+ "macd_fast": macd_fast,
+ "macd_slow": macd_slow,
+ "macd_signal": macd_signal,
+ "stop_loss": sl,
+ "take_profit": tp,
+ "time_limit": time_limit,
+ "trailing_stop": {
+ "activation_price": ts_ap,
+ "trailing_delta": ts_delta
+ },
+ "take_profit_order_type": take_profit_order_type.value
+ }
diff --git a/frontend/pages/config/pmm_dynamic/README.md b/frontend/pages/config/pmm_dynamic/README.md
new file mode 100644
index 0000000..c16ce62
--- /dev/null
+++ b/frontend/pages/config/pmm_dynamic/README.md
@@ -0,0 +1,62 @@
+# PMM Dynamic Configuration Tool
+
+Welcome to the PMM Dynamic Configuration Tool! This tool allows you to create, modify, visualize, backtest, and save configurations for the PMM Dynamic trading strategy. Hereβs how you can make the most out of it.
+
+## Features
+
+- **Start from Default Configurations**: Begin with a default configuration or use the values from an existing configuration.
+- **Modify Configuration Values**: Change various parameters of the configuration to suit your trading strategy.
+- **Visualize Results**: See the impact of your changes through visual charts, including indicators like MACD and NATR.
+- **Backtest Your Strategy**: Run backtests to evaluate the performance of your strategy.
+- **Save and Deploy**: Once satisfied, save the configuration to deploy it later.
+
+## How to Use
+
+### 1. Load Default Configuration
+
+Start by loading the default configuration for the PMM Dynamic strategy. This provides a baseline setup that you can customize to fit your needs.
+
+### 2. User Inputs
+
+Input various parameters for the strategy configuration. These parameters include:
+
+- **Connector Name**: Select the trading platform or exchange.
+- **Trading Pair**: Choose the cryptocurrency trading pair.
+- **Leverage**: Set the leverage ratio. (Note: if you are using spot trading, set the leverage to 1)
+- **Total Amount (Quote Currency)**: Define the total amount you want to allocate for trading.
+- **Position Mode**: Choose between different position modes.
+- **Cooldown Time**: Set the cooldown period between trades.
+- **Executor Refresh Time**: Define how often the executors refresh.
+- **Candles Connector**: Select the data source for candlestick data.
+- **Candles Trading Pair**: Choose the trading pair for candlestick data.
+- **Interval**: Set the interval for candlestick data.
+- **MACD Fast Period**: Set the fast period for the MACD indicator.
+- **MACD Slow Period**: Set the slow period for the MACD indicator.
+- **MACD Signal Period**: Set the signal period for the MACD indicator.
+- **NATR Length**: Define the length for the NATR indicator.
+- **Risk Management**: Set parameters for stop loss, take profit, time limit, and trailing stop settings.
+
+### 3. Indicator Visualization
+
+Visualize the candlestick data along with the MACD and NATR indicators. This helps you understand how the MACD will shift the mid-price and how the NATR will be used as a base multiplier for spreads.
+
+### 4. Executor Distribution
+
+The distribution of orders is now a multiplier of the base spread, which is determined by the NATR indicator. This allows the algorithm to adapt to changing market conditions by adjusting the spread based on the average size of the candles.
+
+### 5. Backtesting
+
+Run backtests to evaluate the performance of your configured strategy. The backtesting section allows you to:
+
+- **Process Data**: Analyze historical trading data.
+- **Visualize Results**: See performance metrics and charts.
+- **Evaluate Accuracy**: Assess the accuracy of your strategyβs predictions and trades.
+- **Understand Close Types**: Review different types of trade closures and their frequencies.
+
+### 6. Save Configuration
+
+Once you are satisfied with your configuration and backtest results, save the configuration for future use in the Deploy tab. This allows you to deploy the same strategy later without having to reconfigure it from scratch.
+
+---
+
+Feel free to experiment with different configurations to find the optimal setup for your trading strategy. Happy trading!
\ No newline at end of file
diff --git a/frontend/pages/config/pmm_dynamic/__init__.py b/frontend/pages/config/pmm_dynamic/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/pmm_dynamic/app.py b/frontend/pages/config/pmm_dynamic/app.py
new file mode 100644
index 0000000..d3e71e5
--- /dev/null
+++ b/frontend/pages/config/pmm_dynamic/app.py
@@ -0,0 +1,87 @@
+import streamlit as st
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+
+from backend.services.backend_api_client import BackendAPIClient
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from frontend.components.config_loader import get_default_config_loader
+from frontend.components.executors_distribution import get_executors_distribution_inputs
+from frontend.components.save_config import render_save_config
+
+# Import submodules
+from frontend.components.backtesting import backtesting_section
+from frontend.pages.config.pmm_dynamic.spread_and_price_multipliers import get_pmm_dynamic_multipliers
+from frontend.pages.config.pmm_dynamic.user_inputs import user_inputs
+from frontend.pages.config.utils import get_max_records, get_candles
+from frontend.st_utils import initialize_st_page
+from frontend.visualization import theme
+from frontend.visualization.backtesting import create_backtesting_figure
+from frontend.visualization.candles import get_candlestick_trace
+from frontend.visualization.executors_distribution import create_executors_distribution_traces
+from frontend.visualization.backtesting_metrics import render_backtesting_metrics, render_close_types, \
+ render_accuracy_metrics
+from frontend.visualization.indicators import get_macd_traces
+from frontend.visualization.utils import add_traces_to_fig
+
+# Initialize the Streamlit page
+initialize_st_page(title="PMM Dynamic", icon="π©βπ«")
+backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+
+# Page content
+st.text("This tool will let you create a config for PMM Dynamic, backtest and upload it to the Backend API.")
+get_default_config_loader("pmm_dynamic")
+# Get user inputs
+inputs = user_inputs()
+st.write("### Visualizing MACD and NATR indicators for PMM Dynamic")
+st.text("The MACD is used to shift the mid price and the NATR to make the spreads dynamic. "
+ "In the order distributions graph, we are going to see the values of the orders affected by the average NATR")
+days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=3)
+# Load candle data
+candles = get_candles(connector_name=inputs["candles_connector"], trading_pair=inputs["candles_trading_pair"], interval=inputs["interval"], days=days_to_visualize)
+with st.expander("Visualizing PMM Dynamic Indicators", expanded=True):
+ fig = make_subplots(rows=4, cols=1, shared_xaxes=True,
+ vertical_spacing=0.02, subplot_titles=('Candlestick with Bollinger Bands', 'MACD', "Price Multiplier", "Spreads Multiplier"),
+ row_heights=[0.8, 0.2, 0.2, 0.2])
+ add_traces_to_fig(fig, [get_candlestick_trace(candles)], row=1, col=1)
+ add_traces_to_fig(fig, get_macd_traces(df=candles, macd_fast=inputs["macd_fast"], macd_slow=inputs["macd_slow"], macd_signal=inputs["macd_signal"]), row=2, col=1)
+ price_multiplier, spreads_multiplier = get_pmm_dynamic_multipliers(candles, inputs["macd_fast"], inputs["macd_slow"], inputs["macd_signal"], inputs["natr_length"])
+ add_traces_to_fig(fig, [go.Scatter(x=candles.index, y=price_multiplier, name="Price Multiplier", line=dict(color="blue"))], row=3, col=1)
+ add_traces_to_fig(fig, [go.Scatter(x=candles.index, y=spreads_multiplier, name="Base Spread", line=dict(color="red"))], row=4, col=1)
+ fig.update_layout(**theme.get_default_layout(height=1000))
+ fig.update_yaxes(tickformat=".2%", row=3, col=1)
+ fig.update_yaxes(tickformat=".2%", row=4, col=1)
+ st.plotly_chart(fig, use_container_width=True)
+
+st.write("### Executors Distribution")
+st.write("The order distributions are affected by the average NATR. This means that if the first order has a spread of "
+ "1 and the NATR is 0.005, the first order will have a spread of 0.5% of the mid price.")
+buy_spread_distributions, sell_spread_distributions, buy_order_amounts_pct, sell_order_amounts_pct = get_executors_distribution_inputs(default_spreads=[1, 2], default_amounts=[1, 2])
+inputs["buy_spreads"] = buy_spread_distributions
+inputs["sell_spreads"] = sell_spread_distributions
+inputs["buy_amounts_pct"] = buy_order_amounts_pct
+inputs["sell_amounts_pct"] = sell_order_amounts_pct
+st.session_state["default_config"] = inputs
+with st.expander("Executor Distribution:", expanded=True):
+ natr_avarage = spreads_multiplier.mean()
+ buy_spreads = [spread * natr_avarage for spread in inputs["buy_spreads"]]
+ sell_spreads = [spread * natr_avarage for spread in inputs["sell_spreads"]]
+ st.write(f"Average NATR: {natr_avarage:.2%}")
+ fig = create_executors_distribution_traces(buy_spreads, sell_spreads, inputs["buy_amounts_pct"], inputs["sell_amounts_pct"], inputs["total_amount_quote"])
+ st.plotly_chart(fig, use_container_width=True)
+
+bt_results = backtesting_section(inputs, backend_api_client)
+if bt_results:
+ fig = create_backtesting_figure(
+ df=bt_results["processed_data"],
+ executors=bt_results["executors"],
+ config=inputs)
+ c1, c2 = st.columns([0.9, 0.1])
+ with c1:
+ render_backtesting_metrics(bt_results["results"])
+ st.plotly_chart(fig, use_container_width=True)
+ with c2:
+ render_accuracy_metrics(bt_results["results"])
+ st.write("---")
+ render_close_types(bt_results["results"])
+st.write("---")
+render_save_config("pmm_dynamic", inputs)
diff --git a/frontend/pages/config/pmm_dynamic/spread_and_price_multipliers.py b/frontend/pages/config/pmm_dynamic/spread_and_price_multipliers.py
new file mode 100644
index 0000000..dfb85f2
--- /dev/null
+++ b/frontend/pages/config/pmm_dynamic/spread_and_price_multipliers.py
@@ -0,0 +1,17 @@
+import pandas_ta as ta # noqa: F401
+
+
+def get_pmm_dynamic_multipliers(df, macd_fast, macd_slow, macd_signal, natr_length):
+ """
+ Get the spread and price multipliers for PMM Dynamic
+ """
+ natr = ta.natr(df["high"], df["low"], df["close"], length=natr_length) / 100
+ macd_output = ta.macd(df["close"], fast=macd_fast,
+ slow=macd_slow, signal=macd_signal)
+ macd = macd_output[f"MACD_{macd_fast}_{macd_slow}_{macd_signal}"]
+ macdh = macd_output[f"MACDh_{macd_fast}_{macd_slow}_{macd_signal}"]
+ macd_signal = - (macd - macd.mean()) / macd.std()
+ macdh_signal = macdh.apply(lambda x: 1 if x > 0 else -1)
+ max_price_shift = natr / 2
+ price_multiplier = ((0.5 * macd_signal + 0.5 * macdh_signal) * max_price_shift)
+ return price_multiplier, natr
diff --git a/frontend/pages/config/pmm_dynamic/user_inputs.py b/frontend/pages/config/pmm_dynamic/user_inputs.py
new file mode 100644
index 0000000..21e7736
--- /dev/null
+++ b/frontend/pages/config/pmm_dynamic/user_inputs.py
@@ -0,0 +1,56 @@
+import streamlit as st
+
+from frontend.components.market_making_general_inputs import get_market_making_general_inputs
+from frontend.components.risk_management import get_risk_management_inputs
+
+
+def user_inputs():
+ default_config = st.session_state.get("default_config", {})
+ macd_fast = default_config.get("macd_fast", 21)
+ macd_slow = default_config.get("macd_slow", 42)
+ macd_signal = default_config.get("macd_signal", 9)
+ natr_length = default_config.get("natr_length", 14)
+ connector_name, trading_pair, leverage, total_amount_quote, position_mode, cooldown_time, executor_refresh_time, candles_connector, candles_trading_pair, interval = get_market_making_general_inputs(custom_candles=True)
+ sl, tp, time_limit, ts_ap, ts_delta, take_profit_order_type = get_risk_management_inputs()
+ with st.expander("PMM Dynamic Configuration", expanded=True):
+ c1, c2, c3, c4 = st.columns(4)
+ with c1:
+ macd_fast = st.number_input("MACD Fast Period", min_value=1, max_value=200, value=macd_fast)
+ with c2:
+ macd_slow = st.number_input("MACD Slow Period", min_value=1, max_value=200, value=macd_slow)
+ with c3:
+ macd_signal = st.number_input("MACD Signal Period", min_value=1, max_value=200, value=macd_signal)
+ with c4:
+ natr_length = st.number_input("NATR Length", min_value=1, max_value=200, value=natr_length)
+
+ # Create the config
+ config = {
+ "controller_name": "pmm_dynamic",
+ "controller_type": "market_making",
+ "manual_kill_switch": None,
+ "candles_config": [],
+ "connector_name": connector_name,
+ "trading_pair": trading_pair,
+ "total_amount_quote": total_amount_quote,
+ "executor_refresh_time": executor_refresh_time,
+ "cooldown_time": cooldown_time,
+ "leverage": leverage,
+ "position_mode": position_mode,
+ "candles_connector": candles_connector,
+ "candles_trading_pair": candles_trading_pair,
+ "interval": interval,
+ "macd_fast": macd_fast,
+ "macd_slow": macd_slow,
+ "macd_signal": macd_signal,
+ "natr_length": natr_length,
+ "stop_loss": sl,
+ "take_profit": tp,
+ "time_limit": time_limit,
+ "take_profit_order_type": take_profit_order_type.value,
+ "trailing_stop": {
+ "activation_price": ts_ap,
+ "trailing_delta": ts_delta
+ }
+ }
+
+ return config
diff --git a/frontend/pages/config/pmm_simple/README.md b/frontend/pages/config/pmm_simple/README.md
new file mode 100644
index 0000000..4b3640d
--- /dev/null
+++ b/frontend/pages/config/pmm_simple/README.md
@@ -0,0 +1,49 @@
+# PMM Simple Configuration Tool
+
+Welcome to the PMM Simple Configuration Tool! This tool allows you to create, modify, visualize, backtest, and save configurations for the PMM Simple trading strategy. Hereβs how you can make the most out of it.
+
+## Features
+
+- **Start from Default Configurations**: Begin with a default configuration or use the values from an existing configuration.
+- **Modify Configuration Values**: Change various parameters of the configuration to suit your trading strategy.
+- **Visualize Results**: See the impact of your changes through visual charts.
+- **Backtest Your Strategy**: Run backtests to evaluate the performance of your strategy.
+- **Save and Deploy**: Once satisfied, save the configuration to deploy it later.
+
+## How to Use
+
+### 1. Load Default Configuration
+
+Start by loading the default configuration for the PMM Simple strategy. This provides a baseline setup that you can customize to fit your needs.
+
+### 2. User Inputs
+
+Input various parameters for the strategy configuration. These parameters include:
+
+- **Connector Name**: Select the trading platform or exchange.
+- **Trading Pair**: Choose the cryptocurrency trading pair.
+- **Leverage**: Set the leverage ratio. (Note: if you are using spot trading, set the leverage to 1)
+- **Total Amount (Quote Currency)**: Define the total amount you want to allocate for trading.
+- **Position Mode**: Choose between different position modes.
+- **Cooldown Time**: Set the cooldown period between trades.
+- **Executor Refresh Time**: Define how often the executors refresh.
+- **Buy/Sell Spread Distributions**: Configure the distribution of buy and sell spreads.
+- **Order Amounts**: Specify the percentages for buy and sell order amounts.
+- **Risk Management**: Set parameters for stop loss, take profit, time limit, and trailing stop settings.
+
+### 3. Executor Distribution Visualization
+
+Visualize the distribution of your trading executors. This helps you understand how your buy and sell orders are spread across different price levels and amounts.
+
+### 4. Backtesting
+
+Run backtests to evaluate the performance of your configured strategy. The backtesting section allows you to:
+
+- **Process Data**: Analyze historical trading data.
+- **Visualize Results**: See performance metrics and charts.
+- **Evaluate Accuracy**: Assess the accuracy of your strategyβs predictions and trades.
+- **Understand Close Types**: Review different types of trade closures and their frequencies.
+
+### 5. Save Configuration
+
+Once you are satisfied with your configuration and backtest results, save the configuration for future use in the Deploy tab. This allows you to deploy the same strategy later without having to reconfigure it from scratch.
\ No newline at end of file
diff --git a/frontend/pages/config/pmm_simple/__init__.py b/frontend/pages/config/pmm_simple/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/pmm_simple/app.py b/frontend/pages/config/pmm_simple/app.py
new file mode 100644
index 0000000..28b9c75
--- /dev/null
+++ b/frontend/pages/config/pmm_simple/app.py
@@ -0,0 +1,44 @@
+import streamlit as st
+from backend.services.backend_api_client import BackendAPIClient
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from frontend.components.config_loader import get_default_config_loader
+from frontend.components.save_config import render_save_config
+
+# Import submodules
+from frontend.pages.config.pmm_simple.user_inputs import user_inputs
+from frontend.components.backtesting import backtesting_section
+from frontend.st_utils import initialize_st_page
+from frontend.visualization.backtesting import create_backtesting_figure
+from frontend.visualization.executors_distribution import create_executors_distribution_traces
+from frontend.visualization.backtesting_metrics import render_backtesting_metrics, render_close_types, \
+ render_accuracy_metrics
+
+# Initialize the Streamlit page
+initialize_st_page(title="PMM Simple", icon="π¨βπ«")
+backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+
+# Page content
+st.text("This tool will let you create a config for PMM Simple, backtest and upload it to the Backend API.")
+get_default_config_loader("pmm_simple")
+inputs = user_inputs()
+
+with st.expander("Executor Distribution:", expanded=True):
+ fig = create_executors_distribution_traces(inputs["buy_spreads"], inputs["sell_spreads"], inputs["buy_amounts_pct"], inputs["sell_amounts_pct"], inputs["total_amount_quote"])
+ st.plotly_chart(fig, use_container_width=True)
+
+bt_results = backtesting_section(inputs, backend_api_client)
+if bt_results:
+ fig = create_backtesting_figure(
+ df=bt_results["processed_data"],
+ executors=bt_results["executors"],
+ config=inputs)
+ c1, c2 = st.columns([0.9, 0.1])
+ with c1:
+ render_backtesting_metrics(bt_results["results"])
+ st.plotly_chart(fig, use_container_width=True)
+ with c2:
+ render_accuracy_metrics(bt_results["results"])
+ st.write("---")
+ render_close_types(bt_results["results"])
+st.write("---")
+render_save_config("pmm_simple", inputs)
diff --git a/frontend/pages/config/pmm_simple/user_inputs.py b/frontend/pages/config/pmm_simple/user_inputs.py
new file mode 100644
index 0000000..69ff319
--- /dev/null
+++ b/frontend/pages/config/pmm_simple/user_inputs.py
@@ -0,0 +1,39 @@
+import streamlit as st
+
+from frontend.components.executors_distribution import get_executors_distribution_inputs
+from frontend.components.market_making_general_inputs import get_market_making_general_inputs
+from frontend.components.risk_management import get_risk_management_inputs
+
+
+def user_inputs():
+ connector_name, trading_pair, leverage, total_amount_quote, position_mode, cooldown_time, executor_refresh_time, _, _, _ = get_market_making_general_inputs()
+ buy_spread_distributions, sell_spread_distributions, buy_order_amounts_pct, sell_order_amounts_pct = get_executors_distribution_inputs()
+ sl, tp, time_limit, ts_ap, ts_delta, take_profit_order_type = get_risk_management_inputs()
+ # Create the config
+ config = {
+ "controller_name": "pmm_simple",
+ "controller_type": "market_making",
+ "manual_kill_switch": None,
+ "candles_config": [],
+ "connector_name": connector_name,
+ "trading_pair": trading_pair,
+ "total_amount_quote": total_amount_quote,
+ "buy_spreads": buy_spread_distributions,
+ "sell_spreads": sell_spread_distributions,
+ "buy_amounts_pct": buy_order_amounts_pct,
+ "sell_amounts_pct": sell_order_amounts_pct,
+ "executor_refresh_time": executor_refresh_time,
+ "cooldown_time": cooldown_time,
+ "leverage": leverage,
+ "position_mode": position_mode,
+ "stop_loss": sl,
+ "take_profit": tp,
+ "time_limit": time_limit,
+ "take_profit_order_type": take_profit_order_type.value,
+ "trailing_stop": {
+ "activation_price": ts_ap,
+ "trailing_delta": ts_delta
+ }
+ }
+ st.session_state["default_config"] = config
+ return config
diff --git a/frontend/pages/config/position_builder/README.md b/frontend/pages/config/position_builder/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/position_builder/__init__.py b/frontend/pages/config/position_builder/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/position_builder/app.py b/frontend/pages/config/position_builder/app.py
similarity index 73%
rename from pages/position_builder/app.py
rename to frontend/pages/config/position_builder/app.py
index 7667caa..8feb50a 100644
--- a/pages/position_builder/app.py
+++ b/frontend/pages/config/position_builder/app.py
@@ -1,15 +1,14 @@
-from math import exp
import streamlit as st
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from decimal import Decimal
import yaml
-from utils.st_utils import initialize_st_page
-from hummingbot.smart_components.utils.distributions import Distributions
+from frontend.components.st_inputs import normalize, distribution_inputs, get_distribution
+from frontend.st_utils import initialize_st_page
# Initialize the Streamlit page
-initialize_st_page(title="Position Generator", icon="π", initial_sidebar_state="collapsed")
+initialize_st_page(title="Position Generator", icon="π")
# Page content
st.text("This tool will help you analyze and generate a position config.")
@@ -18,12 +17,6 @@ st.write("---")
# Layout in columns
col_quote, col_tp_sl, col_levels, col_spread_dist, col_amount_dist = st.columns([1, 1, 1, 2, 2])
-
-def normalize(values):
- total = sum(values)
- return [val / total for val in values]
-
-
def convert_to_yaml(spreads, order_amounts):
data = {
'dca_spreads': [float(spread)/100 for spread in spreads],
@@ -43,61 +36,9 @@ with col_levels:
n_levels = st.number_input("Number of Levels", min_value=1, value=5)
-def distribution_inputs(column, dist_type_name):
- if dist_type_name == "Spread":
- dist_type = column.selectbox(
- f"Type of {dist_type_name} Distribution",
- ("GeoCustom", "Geometric", "Fibonacci", "Manual", "Logarithmic", "Arithmetic"),
- key=f"{dist_type_name.lower()}_dist_type",
- # Set the default value
- )
- else:
- dist_type = column.selectbox(
- f"Type of {dist_type_name} Distribution",
- ("Geometric", "Fibonacci", "Manual", "Logarithmic", "Arithmetic"),
- key=f"{dist_type_name.lower()}_dist_type",
- # Set the default value
- )
- base, scaling_factor, step, ratio, manual_values = None, None, None, None, None
-
- if dist_type != "Manual":
- start = column.number_input(f"{dist_type_name} Start Value", value=1.0, key=f"{dist_type_name.lower()}_start")
- if dist_type == "Logarithmic":
- base = column.number_input(f"{dist_type_name} Log Base", value=exp(1), key=f"{dist_type_name.lower()}_base")
- scaling_factor = column.number_input(f"{dist_type_name} Scaling Factor", value=2.0, key=f"{dist_type_name.lower()}_scaling")
- elif dist_type == "Arithmetic":
- step = column.number_input(f"{dist_type_name} Step", value=0.1, key=f"{dist_type_name.lower()}_step")
- elif dist_type == "Geometric":
- ratio = column.number_input(f"{dist_type_name} Ratio", value=2.0, key=f"{dist_type_name.lower()}_ratio")
- elif dist_type == "GeoCustom":
- ratio = column.number_input(f"{dist_type_name} Ratio", value=2.0, key=f"{dist_type_name.lower()}_ratio")
- else:
- manual_values = [column.number_input(f"{dist_type_name} for level {i+1}", value=1.0, key=f"{dist_type_name.lower()}_{i}") for i in range(n_levels)]
- start = None # As start is not relevant for Manual type
-
- return dist_type, start, base, scaling_factor, step, ratio, manual_values
-
-
# Spread and Amount Distributions
-spread_dist_type, spread_start, spread_base, spread_scaling, spread_step, spread_ratio, manual_spreads = distribution_inputs(col_spread_dist, "Spread")
-amount_dist_type, amount_start, amount_base, amount_scaling, amount_step, amount_ratio, manual_amounts = distribution_inputs(col_amount_dist, "Amount")
-
-
-def get_distribution(dist_type, n_levels, start, base=None, scaling_factor=None, step=None, ratio=None, manual_values=None):
- if dist_type == "Manual":
- return manual_values
- elif dist_type == "Linear":
- return Distributions.linear(n_levels, start, start + tp)
- elif dist_type == "Fibonacci":
- return Distributions.fibonacci(n_levels, start)
- elif dist_type == "Logarithmic":
- return Distributions.logarithmic(n_levels, base, scaling_factor, start)
- elif dist_type == "Arithmetic":
- return Distributions.arithmetic(n_levels, start, step)
- elif dist_type == "Geometric":
- return Distributions.geometric(n_levels, start, ratio)
- elif dist_type == "GeoCustom":
- return [Decimal("0")] + Distributions.geometric(n_levels - 1, start, ratio)
+spread_dist_type, spread_start, spread_base, spread_scaling, spread_step, spread_ratio, manual_spreads = distribution_inputs(col_spread_dist, "Spread", n_levels)
+amount_dist_type, amount_start, amount_base, amount_scaling, amount_step, amount_ratio, manual_amounts = distribution_inputs(col_amount_dist, "Amount", n_levels)
spread_distribution = get_distribution(spread_dist_type, n_levels, spread_start, spread_base, spread_scaling, spread_step, spread_ratio, manual_spreads)
amount_distribution = normalize(get_distribution(amount_dist_type, n_levels, amount_start, amount_base, amount_scaling, amount_step, amount_ratio, manual_amounts))
diff --git a/frontend/pages/config/supertrend_v1/README.md b/frontend/pages/config/supertrend_v1/README.md
new file mode 100644
index 0000000..f93bf3c
--- /dev/null
+++ b/frontend/pages/config/supertrend_v1/README.md
@@ -0,0 +1,72 @@
+# Super Trend Configuration Tool
+
+Welcome to the Super Trend Configuration Tool! This tool allows you to create, modify, visualize, backtest, and save configurations for the Super Trend directional trading strategy. Hereβs how you can make the most out of it.
+
+## Features
+
+- **Start from Default Configurations**: Begin with a default configuration or use the values from an existing configuration.
+- **Modify Configuration Values**: Change various parameters of the configuration to suit your trading strategy.
+- **Visualize Results**: See the impact of your changes through visual charts.
+- **Backtest Your Strategy**: Run backtests to evaluate the performance of your strategy.
+- **Save and Deploy**: Once satisfied, save the configuration to deploy it later.
+
+## How to Use
+
+### 1. Load Default Configuration
+
+Start by loading the default configuration for the Super Trend strategy. This provides a baseline setup that you can customize to fit your needs.
+
+### 2. User Inputs
+
+Input various parameters for the strategy configuration. These parameters include:
+
+- **Connector Name**: Select the trading platform or exchange.
+- **Trading Pair**: Choose the cryptocurrency trading pair.
+- **Leverage**: Set the leverage ratio. (Note: if you are using spot trading, set the leverage to 1)
+- **Total Amount (Quote Currency)**: Define the total amount you want to allocate for trading.
+- **Max Executors per Side**: Specify the maximum number of executors per side.
+- **Cooldown Time**: Set the cooldown period between trades.
+- **Position Mode**: Choose between different position modes.
+- **Candles Connector**: Select the data source for candlestick data.
+- **Candles Trading Pair**: Choose the trading pair for candlestick data.
+- **Interval**: Set the interval for candlestick data.
+- **Super Trend Length**: Define the length of the Super Trend indicator.
+- **Super Trend Multiplier**: Set the multiplier for the Super Trend indicator.
+- **Percentage Threshold**: Set the percentage threshold for signal generation.
+- **Risk Management**: Set parameters for stop loss, take profit, time limit, and trailing stop settings.
+
+### 3. Visualize Indicators
+
+Visualize the Super Trend indicator on the OHLC (Open, High, Low, Close) chart to see the impact of your configuration. Here are some hints to help you fine-tune the indicators:
+
+- **Super Trend Length**: A larger length will make the Super Trend indicator smoother and less sensitive to short-term price fluctuations, while a smaller length will make it more responsive to recent price changes.
+- **Super Trend Multiplier**: Adjusting the multiplier affects the sensitivity of the Super Trend indicator. A higher multiplier makes the trend detection more conservative, while a lower multiplier makes it more aggressive.
+- **Percentage Threshold**: This defines how close the price needs to be to the Super Trend band to generate a signal. For example, a 0.5% threshold means the price needs to be within 0.5% of the Super Trend band to consider a trade.
+
+### Combining Super Trend and Percentage Threshold for Trade Signals
+
+The Super Trend V1 strategy uses the Super Trend indicator combined with a percentage threshold to generate trade signals:
+
+- **Long Signal**: The Super Trend indicator must signal a long trend, and the price must be within the percentage threshold of the Super Trend long band. For example, if the threshold is 0.5%, the price must be within 0.5% of the Super Trend long band to trigger a long trade.
+- **Short Signal**: The Super Trend indicator must signal a short trend, and the price must be within the percentage threshold of the Super Trend short band. Similarly, if the threshold is 0.5%, the price must be within 0.5% of the Super Trend short band to trigger a short trade.
+
+### 4. Executor Distribution
+
+The total amount in the quote currency will be distributed among the maximum number of executors per side. For example, if the total amount quote is 1000 and the max executors per side is 5, each executor will have 200 to trade. If the signal is on, the first executor will place an order and wait for the cooldown time before the next one executes, continuing this pattern for the subsequent orders.
+
+### 5. Backtesting
+
+Run backtests to evaluate the performance of your configured strategy. The backtesting section allows you to:
+
+- **Process Data**: Analyze historical trading data.
+- **Visualize Results**: See performance metrics and charts.
+- **Evaluate Accuracy**: Assess the accuracy of your strategyβs predictions and trades.
+- **Understand Close Types**: Review different types of trade closures and their frequencies.
+
+### 6. Save Configuration
+
+Once you are satisfied with your configuration and backtest results, save the configuration for future use in the Deploy tab. This allows you to deploy the same strategy later without having to reconfigure it from scratch.
+
+---
+
+Feel free to experiment with different configurations to find the optimal setup for your trading strategy. Happy trading!
\ No newline at end of file
diff --git a/frontend/pages/config/supertrend_v1/__init__.py b/frontend/pages/config/supertrend_v1/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/supertrend_v1/app.py b/frontend/pages/config/supertrend_v1/app.py
new file mode 100644
index 0000000..af195d3
--- /dev/null
+++ b/frontend/pages/config/supertrend_v1/app.py
@@ -0,0 +1,64 @@
+import streamlit as st
+from plotly.subplots import make_subplots
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.components.backtesting import backtesting_section
+from frontend.components.config_loader import get_default_config_loader
+from frontend.components.save_config import render_save_config
+from frontend.pages.config.supertrend_v1.user_inputs import user_inputs
+from frontend.pages.config.utils import get_candles, get_max_records
+from frontend.st_utils import initialize_st_page
+from frontend.visualization import theme
+from frontend.visualization.backtesting import create_backtesting_figure
+from frontend.visualization.backtesting_metrics import render_backtesting_metrics, render_accuracy_metrics, \
+ render_close_types
+from frontend.visualization.candles import get_candlestick_trace
+from frontend.visualization.indicators import get_volume_trace, get_supertrend_traces
+from frontend.visualization.signals import get_supertrend_v1_signal_traces
+from frontend.visualization.utils import add_traces_to_fig
+
+# Initialize the Streamlit page
+initialize_st_page(title="SuperTrend V1", icon="π", initial_sidebar_state="expanded")
+backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+
+get_default_config_loader("supertrend_v1")
+# User inputs
+inputs = user_inputs()
+st.session_state["default_config"] = inputs
+
+st.write("### Visualizing Supertrend Trading Signals")
+days_to_visualize = st.number_input("Days to Visualize", min_value=1, max_value=365, value=3)
+# Load candle data
+candles = get_candles(connector_name=inputs["candles_connector"], trading_pair=inputs["candles_trading_pair"], interval=inputs["interval"], days=days_to_visualize)
+
+# Create a subplot with 2 rows
+fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
+ vertical_spacing=0.02, subplot_titles=('Candlestick with Bollinger Bands', 'Volume', "MACD"),
+ row_heights=[0.8, 0.2])
+add_traces_to_fig(fig, [get_candlestick_trace(candles)], row=1, col=1)
+add_traces_to_fig(fig, get_supertrend_traces(candles, inputs["length"], inputs["multiplier"]), row=1, col=1)
+add_traces_to_fig(fig, get_supertrend_v1_signal_traces(candles, inputs["length"], inputs["multiplier"], inputs["percentage_threshold"]), row=1, col=1)
+add_traces_to_fig(fig, [get_volume_trace(candles)], row=2, col=1)
+
+layout_settings = theme.get_default_layout()
+layout_settings["showlegend"] = False
+fig.update_layout(**layout_settings)
+# Use Streamlit's functionality to display the plot
+st.plotly_chart(fig, use_container_width=True)
+bt_results = backtesting_section(inputs, backend_api_client)
+if bt_results:
+ fig = create_backtesting_figure(
+ df=bt_results["processed_data"],
+ executors=bt_results["executors"],
+ config=inputs)
+ c1, c2 = st.columns([0.9, 0.1])
+ with c1:
+ render_backtesting_metrics(bt_results["results"])
+ st.plotly_chart(fig, use_container_width=True)
+ with c2:
+ render_accuracy_metrics(bt_results["results"])
+ st.write("---")
+ render_close_types(bt_results["results"])
+st.write("---")
+render_save_config("bollinger_v1", inputs)
diff --git a/frontend/pages/config/supertrend_v1/user_inputs.py b/frontend/pages/config/supertrend_v1/user_inputs.py
new file mode 100644
index 0000000..05245eb
--- /dev/null
+++ b/frontend/pages/config/supertrend_v1/user_inputs.py
@@ -0,0 +1,46 @@
+import streamlit as st
+from frontend.components.directional_trading_general_inputs import get_directional_trading_general_inputs
+from frontend.components.risk_management import get_risk_management_inputs
+
+
+def user_inputs():
+ default_config = st.session_state.get("default_config", {})
+ length = default_config.get("length", 20)
+ multiplier = default_config.get("multiplier", 3.0)
+ percentage_threshold = default_config.get("percentage_threshold", 0.5)
+ connector_name, trading_pair, leverage, total_amount_quote, max_executors_per_side, cooldown_time, position_mode, candles_connector_name, candles_trading_pair, interval = get_directional_trading_general_inputs()
+ sl, tp, time_limit, ts_ap, ts_delta, take_profit_order_type = get_risk_management_inputs()
+
+ with st.expander("SuperTrend Configuration", expanded=True):
+ c1, c2, c3 = st.columns(3)
+ with c1:
+ length = st.number_input("Supertrend Length", min_value=1, max_value=200, value=length)
+ with c2:
+ multiplier = st.number_input("Supertrend Multiplier", min_value=1.0, max_value=5.0, value=multiplier)
+ with c3:
+ percentage_threshold = st.number_input("Percentage Threshold (%)", value=percentage_threshold) / 100
+ return {
+ "controller_name": "supertrend_v1",
+ "controller_type": "directional_trading",
+ "connector_name": connector_name,
+ "trading_pair": trading_pair,
+ "leverage": leverage,
+ "total_amount_quote": total_amount_quote,
+ "max_executors_per_side": max_executors_per_side,
+ "cooldown_time": cooldown_time,
+ "position_mode": position_mode,
+ "candles_connector": candles_connector_name,
+ "candles_trading_pair": candles_trading_pair,
+ "interval": interval,
+ "length": length,
+ "multiplier": multiplier,
+ "percentage_threshold": percentage_threshold,
+ "stop_loss": sl,
+ "take_profit": tp,
+ "time_limit": time_limit,
+ "trailing_stop": {
+ "activation_price": ts_ap,
+ "trailing_delta": ts_delta
+ },
+ "take_profit_order_type": take_profit_order_type.value
+ }
diff --git a/frontend/pages/config/utils.py b/frontend/pages/config/utils.py
new file mode 100644
index 0000000..cfebbd2
--- /dev/null
+++ b/frontend/pages/config/utils.py
@@ -0,0 +1,28 @@
+import datetime
+
+import streamlit as st
+import pandas as pd
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+
+
+def get_max_records(days_to_download: int, interval: str) -> int:
+ conversion = {"s": 1 / 60, "m": 1, "h": 60, "d": 1440}
+ unit = interval[-1]
+ quantity = int(interval[:-1])
+ return int(days_to_download * 24 * 60 / (quantity * conversion[unit]))
+
+
+@st.cache_data
+def get_candles(connector_name="binance", trading_pair="BTC-USDT", interval="1m", days=7):
+ backend_client = BackendAPIClient(BACKEND_API_HOST, BACKEND_API_PORT)
+ 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() * 1000),
+ end_time=int(end_time.timestamp() * 1000)))
+ df.index = pd.to_datetime(df.timestamp, unit='s')
+ return df
+
diff --git a/frontend/pages/config/xemm_controller/README.md b/frontend/pages/config/xemm_controller/README.md
new file mode 100644
index 0000000..a5ae110
--- /dev/null
+++ b/frontend/pages/config/xemm_controller/README.md
@@ -0,0 +1,60 @@
+# XEMM Configuration Tool
+
+Welcome to the XEMM Configuration Tool! This tool allows you to create, modify, visualize, backtest, and save configurations for the XEMM (Cross-Exchange Market Making) strategy. Hereβs how you can make the most out of it.
+
+## Features
+
+- **Start from Default Configurations**: Begin with a default configuration or use the values from an existing configuration.
+- **Modify Configuration Values**: Change various parameters of the configuration to suit your trading strategy.
+- **Visualize Results**: See the impact of your changes through visual charts.
+- **Backtest Your Strategy**: Run backtests to evaluate the performance of your strategy.
+- **Save and Deploy**: Once satisfied, save the configuration to deploy it later.
+
+## How to Use
+
+### 1. Load Default Configuration
+
+Start by loading the default configuration for the XEMM strategy. This provides a baseline setup that you can customize to fit your needs.
+
+### 2. User Inputs
+
+Input various parameters for the strategy configuration. These parameters include:
+
+- **Maker Connector**: Select the maker trading platform or exchange where limit orders will be placed.
+- **Maker Trading Pair**: Choose the trading pair on the maker exchange.
+- **Taker Connector**: Select the taker trading platform or exchange where market orders will be executed to hedge the imbalance.
+- **Taker Trading Pair**: Choose the trading pair on the taker exchange.
+- **Min Profitability**: Set the minimum profitability percentage at which orders will be refreshed to avoid risking liquidity.
+- **Max Profitability**: Set the maximum profitability percentage at which orders will be refreshed to avoid being too far from the mid-price.
+- **Buy Maker Levels**: Specify the number of buy maker levels.
+- **Buy Targets and Amounts**: Define the target profitability and amounts for each buy maker level.
+- **Sell Maker Levels**: Specify the number of sell maker levels.
+- **Sell Targets and Amounts**: Define the target profitability and amounts for each sell maker level.
+
+### 3. Visualize Order Distribution
+
+Visualize the order distribution with profitability targets using Plotly charts. This helps you understand how your buy and sell orders are distributed across different profitability levels.
+
+### Min and Max Profitability
+
+The XEMM strategy uses min and max profitability bounds to manage the placement of limit orders:
+
+- **Min Profitability**: If the expected profitability of a limit order drops below this value, the order will be refreshed to avoid risking liquidity.
+- **Max Profitability**: If the expected profitability of a limit order exceeds this value, the order will be refreshed to avoid being too far from the mid-price.
+
+### Combining Profitability Targets and Order Amounts
+
+- **Buy Orders**: Configure the target profitability and amounts for each buy maker level. The orders will be refreshed if they fall outside the min and max profitability bounds.
+- **Sell Orders**: Similarly, configure the target profitability and amounts for each sell maker level, with orders being refreshed based on the profitability bounds.
+
+### 4. Save and Download Configuration
+
+Once you have configured your strategy, you can save and download the configuration as a YAML file. This allows you to deploy the strategy later without having to reconfigure it from scratch.
+
+### 5. Upload Configuration to Backend API
+
+You can also upload the configuration directly to the Backend API for immediate deployment. This ensures that your strategy is ready to be executed in real-time.
+
+## Conclusion
+
+By following these steps, you can efficiently configure your XEMM strategy, visualize its potential performance, and deploy it for trading. Feel free to experiment with different configurations to find the optimal setup for your trading needs. Happy trading!
\ No newline at end of file
diff --git a/frontend/pages/config/xemm_controller/__init__.py b/frontend/pages/config/xemm_controller/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/config/xemm_controller/app.py b/frontend/pages/config/xemm_controller/app.py
new file mode 100644
index 0000000..00bd029
--- /dev/null
+++ b/frontend/pages/config/xemm_controller/app.py
@@ -0,0 +1,140 @@
+import streamlit as st
+import plotly.graph_objects as go
+import yaml
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.st_utils import initialize_st_page
+
+# Initialize the Streamlit page
+initialize_st_page(title="XEMM Multiple Levels", icon="β‘οΈ")
+
+# Page content
+st.text("This tool will let you create a config for XEMM Controller and upload it to the BackendAPI.")
+st.write("---")
+c1, c2, c3, c4, c5 = st.columns([1, 1, 1, 1, 1])
+
+with c1:
+ maker_connector = st.text_input("Maker Connector", value="kucoin")
+ maker_trading_pair = st.text_input("Maker Trading Pair", value="LBR-USDT")
+with c2:
+ taker_connector = st.text_input("Taker Connector", value="okx")
+ taker_trading_pair = st.text_input("Taker Trading Pair", value="LBR-USDT")
+with c3:
+ min_profitability = st.number_input("Min Profitability (%)", value=0.2, step=0.01) / 100
+ max_profitability = st.number_input("Max Profitability (%)", value=1.0, step=0.01) / 100
+with c4:
+ buy_maker_levels = st.number_input("Buy Maker Levels", value=1, step=1)
+ buy_targets_amounts = []
+ c41, c42 = st.columns([1, 1])
+ for i in range(buy_maker_levels):
+ with c41:
+ target_profitability = st.number_input(f"Target Profitability {i+1} B% ", value=0.3, step=0.01)
+ with c42:
+ amount = st.number_input(f"Amount {i+1}B Quote", value=10, step=1)
+ buy_targets_amounts.append([target_profitability / 100, amount])
+with c5:
+ sell_maker_levels = st.number_input("Sell Maker Levels", value=1, step=1)
+ sell_targets_amounts = []
+ c51, c52 = st.columns([1, 1])
+ for i in range(sell_maker_levels):
+ with c51:
+ target_profitability = st.number_input(f"Target Profitability {i+1}S %", value=0.3, step=0.001)
+ with c52:
+ amount = st.number_input(f"Amount {i+1} S Quote", value=10, step=1)
+ sell_targets_amounts.append([target_profitability / 100, amount])
+
+
+def create_order_graph(order_type, targets, min_profit, max_profit):
+ # Create a figure
+ fig = go.Figure()
+
+ # Convert profit targets to percentage for x-axis and prepare data for bar chart
+ x_values = [t[0] * 100 for t in targets] # Convert to percentage
+ y_values = [t[1] for t in targets]
+ x_labels = [f"{x:.2f}%" for x in x_values] # Format x labels as strings with percentage sign
+
+ # Add bar plot for visualization of targets
+ fig.add_trace(go.Bar(
+ x=x_labels,
+ y=y_values,
+ width=0.01,
+ name=f'{order_type.capitalize()} Targets',
+ marker=dict(color='gold')
+ ))
+
+ # Convert min and max profitability to percentages for reference lines
+ min_profit_percent = min_profit * 100
+ max_profit_percent = max_profit * 100
+
+ # Add vertical lines for min and max profitability
+ fig.add_shape(type="line",
+ x0=min_profit_percent, y0=0, x1=min_profit_percent, y1=max(y_values, default=10),
+ line=dict(color="red", width=2),
+ name='Min Profitability')
+ fig.add_shape(type="line",
+ x0=max_profit_percent, y0=0, x1=max_profit_percent, y1=max(y_values, default=10),
+ line=dict(color="red", width=2),
+ name='Max Profitability')
+
+ # Update layouts with x-axis starting at 0
+ fig.update_layout(
+ title=f"{order_type.capitalize()} Order Distribution with Profitability Targets",
+ xaxis=dict(
+ title="Profitability (%)",
+ range=[0, max(max(x_values + [min_profit_percent, max_profit_percent]) + 0.1, 1)] # Adjust range to include a buffer
+ ),
+ yaxis=dict(
+ title="Order Amount"
+ ),
+ height=400,
+ width=600
+ )
+
+ return fig
+
+# Use the function for both buy and sell orders
+buy_order_fig = create_order_graph('buy', buy_targets_amounts, min_profitability, max_profitability)
+sell_order_fig = create_order_graph('sell', sell_targets_amounts, min_profitability, max_profitability)
+
+# Display the Plotly graphs in Streamlit
+st.plotly_chart(buy_order_fig, use_container_width=True)
+st.plotly_chart(sell_order_fig, use_container_width=True)
+
+# Display in Streamlit
+c1, c2, c3 = st.columns([2, 2, 1])
+with c1:
+ config_base = st.text_input("Config Base", value=f"xemm_{maker_connector}_{taker_connector}-{maker_trading_pair.split('-')[0]}")
+with c2:
+ config_tag = st.text_input("Config Tag", value="1.1")
+
+id = f"{config_base}-{config_tag}"
+config = {
+ "id": id.lower(),
+ "controller_name": "xemm_multiple_levels",
+ "controller_type": "generic",
+ "maker_connector": maker_connector,
+ "maker_trading_pair": maker_trading_pair,
+ "taker_connector": taker_connector,
+ "taker_trading_pair": taker_trading_pair,
+ "min_profitability": min_profitability,
+ "max_profitability": max_profitability,
+ "buy_levels_targets_amount": buy_targets_amounts,
+ "sell_levels_targets_amount": sell_targets_amounts
+}
+yaml_config = yaml.dump(config, default_flow_style=False)
+
+with c3:
+ download_config = st.download_button(
+ label="Download YAML",
+ data=yaml_config,
+ file_name=f'{id.lower()}.yml',
+ mime='text/yaml'
+ )
+ upload_config_to_backend = st.button("Upload Config to BackendAPI")
+
+
+if upload_config_to_backend:
+ backend_api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+ backend_api_client.add_controller_config(config)
+ st.success("Config uploaded successfully!")
\ No newline at end of file
diff --git a/frontend/pages/data/__init__.py b/frontend/pages/data/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/backtest_get_data/README.md b/frontend/pages/data/download_candles/README.md
similarity index 100%
rename from pages/backtest_get_data/README.md
rename to frontend/pages/data/download_candles/README.md
diff --git a/frontend/pages/data/download_candles/__init__.py b/frontend/pages/data/download_candles/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/data/download_candles/app.py b/frontend/pages/data/download_candles/app.py
new file mode 100644
index 0000000..ee1a7cc
--- /dev/null
+++ b/frontend/pages/data/download_candles/app.py
@@ -0,0 +1,70 @@
+import streamlit as st
+from datetime import datetime, time
+import pandas as pd
+import plotly.graph_objects as go
+
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.st_utils import initialize_st_page
+
+# Initialize Streamlit page
+initialize_st_page(title="Download Candles", icon="πΎ")
+backend_api_client = BackendAPIClient.get_instance()
+
+c1, c2, c3, c4 = st.columns([2, 2, 2, 0.5])
+with c1:
+ connector = st.selectbox("Exchange", ["binance_perpetual", "binance", "gate_io", "gate_io_perpetual", "kucoin", "ascend_ex"], index=0)
+ trading_pair = st.text_input("Trading Pair", value="BTC-USDT")
+with c2:
+ interval = st.selectbox("Interval", options=["1m", "3m", "5m", "15m", "1h", "4h", "1d", "1s"])
+with c3:
+ start_date = st.date_input("Start Date", value=datetime(2023, 1, 1))
+ end_date = st.date_input("End Date", value=datetime(2023, 1, 2))
+with c4:
+ get_data_button = st.button("Get Candles!")
+
+if get_data_button:
+ start_datetime = datetime.combine(start_date, time.min)
+ end_datetime = datetime.combine(end_date, time.max)
+
+ candles = backend_api_client.get_historical_candles(
+ connector=connector,
+ trading_pair=trading_pair,
+ interval=interval,
+ start_time=int(start_datetime.timestamp()) * 1000,
+ end_time=int(end_datetime.timestamp()) * 1000
+ )
+
+ candles_df = pd.DataFrame(candles)
+ candles_df.index = pd.to_datetime(candles_df["timestamp"], unit='s')
+
+ # Plotting the candlestick chart
+ fig = go.Figure(data=[go.Candlestick(
+ x=candles_df.index,
+ open=candles_df['open'],
+ high=candles_df['high'],
+ low=candles_df['low'],
+ close=candles_df['close'],
+ increasing_line_color='#2ECC71',
+ decreasing_line_color='#E74C3C'
+ )])
+ fig.update_layout(
+ height=1000,
+ title="Candlesticks",
+ xaxis_title="Time",
+ yaxis_title="Price",
+ template="plotly_dark",
+ showlegend=False
+ )
+ fig.update_xaxes(rangeslider_visible=False)
+ fig.update_yaxes(title_text="Price")
+ st.plotly_chart(fig, use_container_width=True)
+
+ # Generating CSV and download button
+ csv = candles_df.to_csv(index=False)
+ filename = f"{connector}_{trading_pair}_{start_date.strftime('%Y%m%d')}_{end_date.strftime('%Y%m%d')}.csv"
+ st.download_button(
+ label="Download Candles as CSV",
+ data=csv,
+ file_name=filename,
+ mime='text/csv',
+ )
diff --git a/pages/token_spreads/README.md b/frontend/pages/data/token_spreads/README.md
similarity index 100%
rename from pages/token_spreads/README.md
rename to frontend/pages/data/token_spreads/README.md
diff --git a/frontend/pages/data/token_spreads/__init__.py b/frontend/pages/data/token_spreads/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/token_spreads/app.py b/frontend/pages/data/token_spreads/app.py
similarity index 90%
rename from pages/token_spreads/app.py
rename to frontend/pages/data/token_spreads/app.py
index 1fa8eb0..56a30f6 100644
--- a/pages/token_spreads/app.py
+++ b/frontend/pages/data/token_spreads/app.py
@@ -1,17 +1,15 @@
import streamlit as st
-from pathlib import Path
import plotly.express as px
import CONFIG
-from utils.coingecko_utils import CoinGeckoUtils
-from utils.miner_utils import MinerUtils
-from utils.st_utils import initialize_st_page
-
+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 = CoinGeckoUtils()
-miner_utils = MinerUtils()
+cg_utils = CoinGeckoClient()
+miner_utils = MinerClient()
@st.cache_data
def get_all_coins_df():
diff --git a/pages/tvl_vs_mcap/README.md b/frontend/pages/data/tvl_vs_mcap/README.md
similarity index 100%
rename from pages/tvl_vs_mcap/README.md
rename to frontend/pages/data/tvl_vs_mcap/README.md
diff --git a/frontend/pages/data/tvl_vs_mcap/__init__.py b/frontend/pages/data/tvl_vs_mcap/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/tvl_vs_mcap/app.py b/frontend/pages/data/tvl_vs_mcap/app.py
similarity index 97%
rename from pages/tvl_vs_mcap/app.py
rename to frontend/pages/data/tvl_vs_mcap/app.py
index e553e46..48e276f 100644
--- a/pages/tvl_vs_mcap/app.py
+++ b/frontend/pages/data/tvl_vs_mcap/app.py
@@ -4,8 +4,7 @@ import pandas as pd
import plotly.express as px
from defillama import DefiLlama
-from utils.st_utils import initialize_st_page
-
+from frontend.st_utils import initialize_st_page
initialize_st_page(title="TVL vs Market Cap", icon="π¦")
diff --git a/frontend/pages/orchestration/__init__.py b/frontend/pages/orchestration/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/bot_orchestration/README.md b/frontend/pages/orchestration/credentials/README.md
similarity index 100%
rename from pages/bot_orchestration/README.md
rename to frontend/pages/orchestration/credentials/README.md
diff --git a/frontend/pages/orchestration/credentials/__init__.py b/frontend/pages/orchestration/credentials/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/orchestration/credentials/app.py b/frontend/pages/orchestration/credentials/app.py
new file mode 100644
index 0000000..c719fe3
--- /dev/null
+++ b/frontend/pages/orchestration/credentials/app.py
@@ -0,0 +1,84 @@
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.st_utils import initialize_st_page
+import streamlit as st
+
+
+initialize_st_page(title="Credentials", icon="π")
+
+# Page content
+client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+NUM_COLUMNS = 4
+
+
+@st.cache_data
+def get_all_connectors_config_map():
+ return client.get_all_connectors_config_map()
+
+# Section to display available accounts and credentials
+accounts = client.get_accounts()
+all_connector_config_map = get_all_connectors_config_map()
+st.header("Available Accounts and Credentials")
+
+if accounts:
+ n_accounts = len(accounts)
+ accounts.remove("master_account")
+ accounts.insert(0, "master_account")
+ for i in range(0, n_accounts, NUM_COLUMNS):
+ cols = st.columns(NUM_COLUMNS)
+ for j, account in enumerate(accounts[i:i + NUM_COLUMNS]):
+ with cols[j]:
+ st.subheader(f"π¦ {account}")
+ credentials = client.get_credentials(account)
+ st.json(credentials)
+else:
+ st.write("No accounts available.")
+
+st.markdown("---")
+
+c1, c2 = st.columns([1, 1])
+with c1:
+ # Section to create a new account
+ st.header("Create a New Account")
+ new_account_name = st.text_input("New Account Name")
+ if st.button("Create Account"):
+ if new_account_name:
+ response = client.add_account(new_account_name)
+ st.write(response)
+ else:
+ st.write("Please enter an account name.")
+
+with c2:
+ # Section to delete an existing account
+ st.header("Delete an Account")
+ delete_account_name = st.selectbox("Select Account to Delete", options=accounts if accounts else ["No accounts available"], )
+ if st.button("Delete Account"):
+ if delete_account_name and delete_account_name != "No accounts available":
+ response = client.delete_account(delete_account_name)
+ st.write(response)
+ else:
+ st.write("Please select a valid account.")
+
+st.markdown("---")
+
+# Section to add credentials
+st.header("Add Credentials")
+c1, c2 = st.columns([1, 1])
+with c1:
+ account_name = st.selectbox("Select Account", options=accounts if accounts else ["No accounts available"])
+with c2:
+ all_connectors = list(all_connector_config_map.keys())
+ binance_perpetual_index = all_connectors.index("binance_perpetual") if "binance_perpetual" in all_connectors else None
+ connector_name = st.selectbox("Select Connector", options=all_connectors, index=binance_perpetual_index)
+if account_name and account_name != "No accounts available" and connector_name:
+ st.write(f"Configuration Map for {connector_name}:")
+ config_map = all_connector_config_map[connector_name]
+ config_inputs = {}
+ cols = st.columns(NUM_COLUMNS)
+ for i, config in enumerate(config_map):
+ with cols[i % (NUM_COLUMNS - 1)]:
+ config_inputs[config] = st.text_input(config)
+ with cols[NUM_COLUMNS - 1]:
+ if st.button("Submit Credentials"):
+ response = client.add_connector_keys(account_name, connector_name, config_inputs)
+ st.write(response)
diff --git a/pages/file_manager/README.md b/frontend/pages/orchestration/file_manager/README.md
similarity index 100%
rename from pages/file_manager/README.md
rename to frontend/pages/orchestration/file_manager/README.md
diff --git a/frontend/pages/orchestration/file_manager/__init__.py b/frontend/pages/orchestration/file_manager/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/file_manager/app.py b/frontend/pages/orchestration/file_manager/app.py
similarity index 77%
rename from pages/file_manager/app.py
rename to frontend/pages/orchestration/file_manager/app.py
index 059f472..8f20839 100644
--- a/pages/file_manager/app.py
+++ b/frontend/pages/orchestration/file_manager/app.py
@@ -2,13 +2,12 @@ from types import SimpleNamespace
import streamlit as st
from streamlit_elements import elements, mui
-from ui_components.bots_file_explorer import BotsFileExplorer
-from ui_components.dashboard import Dashboard
-from ui_components.editor import Editor
-from utils.st_utils import initialize_st_page
+from frontend.components.bots_file_explorer import BotsFileExplorer
+from frontend.components.dashboard import Dashboard
+from frontend.components.editor import Editor
+from frontend.st_utils import initialize_st_page
-
-initialize_st_page(title="Strategy Configs", icon="ποΈ", initial_sidebar_state="collapsed")
+initialize_st_page(title="Strategy Configs", icon="ποΈ")
if "fe_board" not in st.session_state:
diff --git a/pages/launch_bot/README.md b/frontend/pages/orchestration/instances/README.md
similarity index 100%
rename from pages/launch_bot/README.md
rename to frontend/pages/orchestration/instances/README.md
diff --git a/frontend/pages/orchestration/instances/__init__.py b/frontend/pages/orchestration/instances/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/orchestration/instances/app.py b/frontend/pages/orchestration/instances/app.py
new file mode 100644
index 0000000..512c65c
--- /dev/null
+++ b/frontend/pages/orchestration/instances/app.py
@@ -0,0 +1,75 @@
+import time
+
+import streamlit as st
+from streamlit_elements import elements, mui
+from types import SimpleNamespace
+
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from frontend.components.bot_performance_card import BotPerformanceCardV2
+from frontend.components.dashboard import Dashboard
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.st_utils import initialize_st_page
+
+# Constants for UI layout
+CARD_WIDTH = 12
+CARD_HEIGHT = 4
+NUM_CARD_COLS = 1
+
+def get_grid_positions(n_cards: int, cols: int = NUM_CARD_COLS, card_width: int = CARD_WIDTH, card_height: int = CARD_HEIGHT):
+ rows = n_cards // cols + 1
+ x_y = [(x * card_width, y * card_height) for x in range(cols) for y in range(rows)]
+ return sorted(x_y, key=lambda x: (x[1], x[0]))
+
+
+def update_active_bots(api_client):
+ active_bots_response = api_client.get_active_bots_status()
+ if active_bots_response.get("status") == "success":
+ current_active_bots = active_bots_response.get("data")
+ stored_bots = {card[1]: card for card in st.session_state.active_instances_board.bot_cards}
+
+ new_bots = set(current_active_bots.keys()) - set(stored_bots.keys())
+ removed_bots = set(stored_bots.keys()) - set(current_active_bots.keys())
+ for bot in removed_bots:
+ st.session_state.active_instances_board.bot_cards = [card for card in st.session_state.active_instances_board.bot_cards if card[1] != bot]
+ positions = get_grid_positions(len(current_active_bots), NUM_CARD_COLS, CARD_WIDTH, CARD_HEIGHT)
+ for bot, (x, y) in zip(new_bots, positions[:len(new_bots)]):
+ card = BotPerformanceCardV2(st.session_state.active_instances_board.dashboard, x, y, CARD_WIDTH, CARD_HEIGHT)
+ st.session_state.active_instances_board.bot_cards.append((card, bot))
+
+
+initialize_st_page(title="Instances", icon="π¦
")
+api_client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+
+if not api_client.is_docker_running():
+ st.warning("Docker is not running. Please start Docker and refresh the page.")
+ st.stop()
+
+if "active_instances_board" not in st.session_state:
+ active_bots_response = api_client.get_active_bots_status()
+ bot_cards = []
+ board = Dashboard()
+ st.session_state.active_instances_board = SimpleNamespace(
+ dashboard=board,
+ bot_cards=bot_cards,
+ )
+ active_bots = active_bots_response.get("data")
+ number_of_bots = len(active_bots)
+ if number_of_bots > 0:
+ positions = get_grid_positions(number_of_bots, NUM_CARD_COLS, CARD_WIDTH, CARD_HEIGHT)
+ for (bot, bot_info), (x, y) in zip(active_bots.items(), positions):
+ bot_status = api_client.get_bot_status(bot)
+ card = BotPerformanceCardV2(board, x, y, CARD_WIDTH, CARD_HEIGHT)
+ st.session_state.active_instances_board.bot_cards.append((card, bot))
+else:
+ update_active_bots(api_client)
+
+with elements("active_instances_board"):
+ with mui.Paper(sx={"padding": "2rem"}, variant="outlined"):
+ mui.Typography("π Local Instances", variant="h5")
+ for card, bot in st.session_state.active_instances_board.bot_cards:
+ with st.session_state.active_instances_board.dashboard():
+ card(bot)
+
+while True:
+ time.sleep(5)
+ st.rerun()
\ No newline at end of file
diff --git a/pages/master_conf/README.md b/frontend/pages/orchestration/launch_bot_v2/README.md
similarity index 100%
rename from pages/master_conf/README.md
rename to frontend/pages/orchestration/launch_bot_v2/README.md
diff --git a/frontend/pages/orchestration/launch_bot_v2/__init__.py b/frontend/pages/orchestration/launch_bot_v2/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/orchestration/launch_bot_v2/app.py b/frontend/pages/orchestration/launch_bot_v2/app.py
new file mode 100644
index 0000000..bf1e15b
--- /dev/null
+++ b/frontend/pages/orchestration/launch_bot_v2/app.py
@@ -0,0 +1,31 @@
+from types import SimpleNamespace
+
+import streamlit as st
+from streamlit_elements import elements, mui
+
+from frontend.components.dashboard import Dashboard
+from frontend.components.launch_strategy_v2 import LaunchStrategyV2
+from frontend.st_utils import initialize_st_page
+
+CARD_WIDTH = 6
+CARD_HEIGHT = 3
+NUM_CARD_COLS = 2
+
+initialize_st_page(title="Launch Bot", icon="π")
+
+if "launch_bots_board" not in st.session_state:
+ board = Dashboard()
+ launch_bots_board = SimpleNamespace(
+ dashboard=board,
+ launch_bot=LaunchStrategyV2(board, 0, 0, 12, 10),
+ )
+ st.session_state.launch_bots_board = launch_bots_board
+
+else:
+ launch_bots_board = st.session_state.launch_bots_board
+
+
+with elements("create_bot"):
+ with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True):
+ with launch_bots_board.dashboard():
+ launch_bots_board.launch_bot()
diff --git a/frontend/pages/orchestration/launch_bot_v2_st/README.md b/frontend/pages/orchestration/launch_bot_v2_st/README.md
new file mode 100644
index 0000000..18f4d94
--- /dev/null
+++ b/frontend/pages/orchestration/launch_bot_v2_st/README.md
@@ -0,0 +1,19 @@
+### Description
+
+This page helps you deploy and manage Hummingbot instances:
+
+- Starting and stopping Hummingbot Broker
+- Creating, starting and stopping bot instances
+- Managing strategy and script files that instances run
+- Fetching status of running instances
+
+### Maintainers
+
+This page is maintained by Hummingbot Foundation as a template other pages:
+
+* [cardosfede](https://github.com/cardosfede)
+* [fengtality](https://github.com/fengtality)
+
+### Wiki
+
+See the [wiki](https://github.com/hummingbot/dashboard/wiki/%F0%9F%90%99-Bot-Orchestration) for more information.
\ No newline at end of file
diff --git a/frontend/pages/orchestration/launch_bot_v2_st/__init__.py b/frontend/pages/orchestration/launch_bot_v2_st/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/orchestration/launch_bot_v2_st/app.py b/frontend/pages/orchestration/launch_bot_v2_st/app.py
new file mode 100644
index 0000000..50c2c08
--- /dev/null
+++ b/frontend/pages/orchestration/launch_bot_v2_st/app.py
@@ -0,0 +1,9 @@
+from frontend.components.deploy_v2_with_controllers import LaunchV2WithControllers
+from frontend.st_utils import initialize_st_page
+
+
+initialize_st_page(title="Launch Bot ST", icon="π")
+
+
+launcher = LaunchV2WithControllers()
+launcher()
diff --git a/frontend/pages/orchestration/portfolio/README.md b/frontend/pages/orchestration/portfolio/README.md
new file mode 100644
index 0000000..18f4d94
--- /dev/null
+++ b/frontend/pages/orchestration/portfolio/README.md
@@ -0,0 +1,19 @@
+### Description
+
+This page helps you deploy and manage Hummingbot instances:
+
+- Starting and stopping Hummingbot Broker
+- Creating, starting and stopping bot instances
+- Managing strategy and script files that instances run
+- Fetching status of running instances
+
+### Maintainers
+
+This page is maintained by Hummingbot Foundation as a template other pages:
+
+* [cardosfede](https://github.com/cardosfede)
+* [fengtality](https://github.com/fengtality)
+
+### Wiki
+
+See the [wiki](https://github.com/hummingbot/dashboard/wiki/%F0%9F%90%99-Bot-Orchestration) for more information.
\ No newline at end of file
diff --git a/frontend/pages/orchestration/portfolio/__init__.py b/frontend/pages/orchestration/portfolio/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/pages/orchestration/portfolio/app.py b/frontend/pages/orchestration/portfolio/app.py
new file mode 100644
index 0000000..286bc1b
--- /dev/null
+++ b/frontend/pages/orchestration/portfolio/app.py
@@ -0,0 +1,58 @@
+from CONFIG import BACKEND_API_HOST, BACKEND_API_PORT
+from backend.services.backend_api_client import BackendAPIClient
+from frontend.st_utils import initialize_st_page
+import streamlit as st
+import pandas as pd
+
+initialize_st_page(title="Portfolio", icon="π°")
+
+# Page content
+client = BackendAPIClient.get_instance(host=BACKEND_API_HOST, port=BACKEND_API_PORT)
+NUM_COLUMNS = 4
+
+@st.cache_data
+def get_all_balances():
+ return client.get_all_balances()
+
+# Fetch all balances
+balances = get_all_balances()
+
+# Convert balances to a DataFrame for easier manipulation
+def balances_to_df(balances):
+ data = []
+ for account, exchanges in balances.items():
+ for exchange, tokens in exchanges.items():
+ for token, amount in tokens.items():
+ data.append({"Account": account, "Exchange": exchange, "Token": token, "Amount": amount})
+ return pd.DataFrame(data)
+
+df_balances = balances_to_df(balances)
+c1, c2 = st.columns([1, 1])
+with c1:
+ st.header("Current Balances")
+with c2:
+ st.header("Aggregated Balances")
+
+c1, c2, c3, c4 = st.columns([2.5, 1.5, 1.5, 1.1])
+with c1:
+ # Display balances
+ st.subheader("All Balances")
+ st.dataframe(df_balances)
+
+with c2:
+ # Aggregation at the account level
+ account_agg = df_balances.groupby(["Account", "Token"])["Amount"].sum().reset_index()
+ st.subheader("Account Level")
+ st.dataframe(account_agg)
+
+with c3:
+ # Aggregation at the exchange level
+ exchange_agg = df_balances.groupby(["Exchange", "Token"])["Amount"].sum().reset_index()
+ st.subheader("Exchange Level")
+ st.dataframe(exchange_agg)
+
+with c4:
+ # Overall holdings
+ overall_agg = df_balances.groupby("Token")["Amount"].sum().reset_index()
+ st.subheader("Token Level")
+ st.write(overall_agg)
diff --git a/frontend/pages/performance/__init__.py b/frontend/pages/performance/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/db_inspector/README.md b/frontend/pages/performance/db_inspector/README.md
similarity index 100%
rename from pages/db_inspector/README.md
rename to frontend/pages/performance/db_inspector/README.md
diff --git a/frontend/pages/performance/db_inspector/__init__.py b/frontend/pages/performance/db_inspector/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/db_inspector/app.py b/frontend/pages/performance/db_inspector/app.py
similarity index 95%
rename from pages/db_inspector/app.py
rename to frontend/pages/performance/db_inspector/app.py
index bde870f..a1a6d61 100644
--- a/pages/db_inspector/app.py
+++ b/frontend/pages/performance/db_inspector/app.py
@@ -3,8 +3,7 @@ import streamlit as st
import sqlite3
import pandas as pd
-from utils.st_utils import initialize_st_page
-
+from frontend.st_utils import initialize_st_page
initialize_st_page(title="DB Inspector", icon="π")
diff --git a/pages/strategy_performance/README.md b/frontend/pages/performance/strategy_performance/README.md
similarity index 100%
rename from pages/strategy_performance/README.md
rename to frontend/pages/performance/strategy_performance/README.md
diff --git a/frontend/pages/performance/strategy_performance/__init__.py b/frontend/pages/performance/strategy_performance/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pages/strategy_performance/app.py b/frontend/pages/performance/strategy_performance/app.py
similarity index 97%
rename from pages/strategy_performance/app.py
rename to frontend/pages/performance/strategy_performance/app.py
index d7ea0c0..b1ccef5 100644
--- a/pages/strategy_performance/app.py
+++ b/frontend/pages/performance/strategy_performance/app.py
@@ -2,11 +2,10 @@ import os
import pandas as pd
import streamlit as st
import math
-from utils.os_utils import get_databases
-from utils.database_manager import DatabaseManager
-from utils.graphs import PerformanceGraphs
-from utils.st_utils import initialize_st_page, download_csv_button, style_metric_cards, db_error_message
+from backend.utils.os_utils import get_databases
+from frontend.visualization.graphs import PerformanceGraphs
+from frontend.st_utils import initialize_st_page, style_metric_cards
initialize_st_page(title="Strategy Performance", icon="π")
style_metric_cards()
diff --git a/utils/st_utils.py b/frontend/st_utils.py
similarity index 81%
rename from utils/st_utils.py
rename to frontend/st_utils.py
index a89dcf5..82575ca 100644
--- a/utils/st_utils.py
+++ b/frontend/st_utils.py
@@ -1,4 +1,5 @@
import os.path
+
import pandas as pd
from pathlib import Path
import inspect
@@ -6,9 +7,8 @@ import inspect
import streamlit as st
from st_pages import add_page_title
-from utils.database_manager import DatabaseManager
-def initialize_st_page(title: str, icon: str, layout="wide", initial_sidebar_state="collapsed"):
+def initialize_st_page(title: str, icon: str, layout="wide", initial_sidebar_state="expanded"):
st.set_page_config(
page_title=title,
page_icon=icon,
@@ -66,13 +66,3 @@ def style_metric_cards(
unsafe_allow_html=True,
)
-
-def db_error_message(db: DatabaseManager, error_message: str):
- container = st.container()
- with container:
- st.warning(error_message)
- with st.expander("DB Status"):
- status_df = pd.DataFrame([db.status]).transpose().reset_index()
- status_df.columns = ["Attribute", "Value"]
- st.table(status_df)
- return container
diff --git a/frontend/visualization/__init__.py b/frontend/visualization/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/visualization/backtesting.py b/frontend/visualization/backtesting.py
new file mode 100644
index 0000000..4d4f3b0
--- /dev/null
+++ b/frontend/visualization/backtesting.py
@@ -0,0 +1,33 @@
+from plotly.subplots import make_subplots
+from frontend.visualization.candles import get_bt_candlestick_trace, get_candlestick_trace
+from frontend.visualization.executors import add_executors_trace
+from frontend.visualization.pnl import get_pnl_trace
+from frontend.visualization.theme import get_default_layout
+
+
+def create_backtesting_figure(df, executors, config):
+ # Create subplots
+ fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
+ vertical_spacing=0.02, subplot_titles=('Candlestick', 'PNL Quote'),
+ row_heights=[0.7, 0.3])
+
+ # Add candlestick trace
+ fig.add_trace(get_bt_candlestick_trace(df), row=1, col=1)
+
+ # Add executors trace
+ fig = add_executors_trace(fig, executors, row=1, col=1)
+
+ # Add PNL trace
+ fig.add_trace(get_pnl_trace(executors), row=2, col=1)
+
+ # Apply the theme layout
+ layout_settings = get_default_layout(f"Trading Pair: {config['trading_pair']}")
+ layout_settings["showlegend"] = False
+ fig.update_layout(**layout_settings)
+
+ # Update axis properties
+ fig.update_xaxes(rangeslider_visible=False, row=1, col=1)
+ fig.update_xaxes(row=2, col=1)
+ fig.update_yaxes(title_text="Price", row=1, col=1)
+ fig.update_yaxes(title_text="PNL", row=2, col=1)
+ return fig
diff --git a/frontend/visualization/backtesting_metrics.py b/frontend/visualization/backtesting_metrics.py
new file mode 100644
index 0000000..016c9df
--- /dev/null
+++ b/frontend/visualization/backtesting_metrics.py
@@ -0,0 +1,63 @@
+import streamlit as st
+
+
+def render_backtesting_metrics(summary_results):
+ net_pnl = summary_results.get('net_pnl', 0)
+ net_pnl_quote = summary_results.get('net_pnl_quote', 0)
+ total_volume = summary_results.get('total_volume', 0)
+ total_executors_with_position = summary_results.get('total_executors_with_position', 0)
+
+ max_drawdown_usd = summary_results.get('max_drawdown_usd', 0)
+ max_drawdown_pct = summary_results.get('max_drawdown_pct', 0)
+ sharpe_ratio = summary_results.get('sharpe_ratio', 0)
+ profit_factor = summary_results.get('profit_factor', 0)
+
+ # Displaying KPIs in Streamlit
+ st.write("### Backtesting Metrics")
+ col1, col2, col3, col4, col5, col6 = st.columns(6)
+ col1.metric(label="Net PNL (Quote)", value=f"{net_pnl_quote:.2f}", delta=f"{net_pnl:.2%}")
+ col2.metric(label="Max Drawdown (USD)", value=f"{max_drawdown_usd:.2f}", delta=f"{max_drawdown_pct:.2%}")
+ col3.metric(label="Total Volume (Quote)", value=f"{total_volume:.2f}")
+ col4.metric(label="Sharpe Ratio", value=f"{sharpe_ratio:.2f}")
+ col5.metric(label="Profit Factor", value=f"{profit_factor:.2f}")
+ col6.metric(label="Total Executors with Position", value=total_executors_with_position)
+
+
+def render_accuracy_metrics(summary_results):
+ accuracy = summary_results.get('accuracy', 0)
+ total_long = summary_results.get('total_long', 0)
+ total_short = summary_results.get('total_short', 0)
+ accuracy_long = summary_results.get('accuracy_long', 0)
+ accuracy_short = summary_results.get('accuracy_short', 0)
+
+ st.write("#### Accuracy Metrics")
+ # col1, col2, col3, col4, col5 = st.columns(5)
+ st.metric(label="Global Accuracy", value=f"{accuracy:.2%}")
+ st.metric(label="Total Long", value=total_long)
+ st.metric(label="Total Short", value=total_short)
+ st.metric(label="Accuracy Long", value=f"{accuracy_long:.2%}")
+ st.metric(label="Accuracy Short", value=f"{accuracy_short:.2%}")
+
+def render_accuracy_metrics2(summary_results):
+ accuracy = summary_results.get('accuracy', 0)
+ total_long = summary_results.get('total_long', 0)
+ total_short = summary_results.get('total_short', 0)
+ accuracy_long = summary_results.get('accuracy_long', 0)
+ accuracy_short = summary_results.get('accuracy_short', 0)
+
+ st.write("#### Accuracy Metrics")
+ col1, col2, col3, col4, col5 = st.columns(5)
+ col1.metric(label="Global Accuracy", value=f"{accuracy}:.2%")
+ col2.metric(label="Total Long", value=total_long)
+ col3.metric(label="Total Short", value=total_short)
+ col4.metric(label="Accuracy Long", value=f"{accuracy_long:.2%}")
+ col5.metric(label="Accuracy Short", value=f"{accuracy_short:.2%}")
+
+
+def render_close_types(summary_results):
+ st.write("#### Close Types")
+ close_types = summary_results.get('close_types', {})
+ st.metric(label="TAKE PROFIT", value=f"{close_types.get('TAKE_PROFIT', 0)}")
+ st.metric(label="STOP LOSS", value=f"{close_types.get('STOP_LOSS', 0)}")
+ st.metric(label="TIME LIMIT", value=f"{close_types.get('TIME_LIMIT', 0)}")
+ st.metric(label="EARLY STOP", value=f"{close_types.get('EARLY_STOP', 0)}")
diff --git a/frontend/visualization/candles.py b/frontend/visualization/candles.py
new file mode 100644
index 0000000..6bbbdda
--- /dev/null
+++ b/frontend/visualization/candles.py
@@ -0,0 +1,23 @@
+import plotly.graph_objects as go
+import pandas as pd
+
+from frontend.visualization import theme
+
+
+def get_candlestick_trace(df):
+ return go.Candlestick(x=df.index,
+ open=df['open'],
+ high=df['high'],
+ low=df['low'],
+ close=df['close'],
+ name="Candlesticks",
+ increasing_line_color='#2ECC71', decreasing_line_color='#E74C3C',)
+
+
+def get_bt_candlestick_trace(df):
+ df.index = pd.to_datetime(df.timestamp, unit='s')
+ return go.Scatter(x=df.index,
+ y=df['close'],
+ mode='lines',
+ line=dict(color=theme.get_color_scheme()["price"]),
+ )
diff --git a/frontend/visualization/dca_builder.py b/frontend/visualization/dca_builder.py
new file mode 100644
index 0000000..bfa3e7b
--- /dev/null
+++ b/frontend/visualization/dca_builder.py
@@ -0,0 +1,161 @@
+from decimal import Decimal
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+import frontend.visualization.theme as theme
+
+
+def calculate_unrealized_pnl(spreads, break_even_values, accumulated_amount):
+ unrealized_pnl = []
+ for i in range(len(spreads)):
+ distance = abs(spreads[i] - break_even_values[i])
+ pnl = accumulated_amount[i] * distance / 100 # PNL calculation
+ unrealized_pnl.append(pnl)
+ return unrealized_pnl
+
+
+def create_dca_graph(dca_inputs, dca_amount):
+ tech_colors = theme.get_color_scheme()
+ dca_order_amounts = [amount_dist * dca_amount for amount_dist in dca_inputs["dca_amounts"]]
+ n_levels = len(dca_inputs["dca_spreads"])
+ dca_spreads = [spread * 100 for spread in dca_inputs["dca_spreads"]]
+
+ break_even_values = []
+ take_profit_values = []
+ for level in range(n_levels):
+ dca_spreads_normalized = [spread + 0.01 for spread in dca_spreads[:level + 1]]
+ amounts = dca_order_amounts[:level + 1]
+ break_even = (sum([spread * amount for spread, amount in zip(dca_spreads_normalized, amounts)]) / sum(
+ amounts)) - 0.01
+ break_even_values.append(break_even)
+ take_profit_values.append(break_even - dca_inputs["take_profit"] * 100)
+
+ accumulated_amount = [sum(dca_order_amounts[:i + 1]) for i in range(len(dca_order_amounts))]
+
+ # Calculate unrealized PNL
+ cum_unrealized_pnl = calculate_unrealized_pnl(dca_spreads, break_even_values, accumulated_amount)
+
+ # Create Plotly figure with secondary y-axis and a dark theme
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
+ fig.update_layout(template="plotly_dark")
+
+ # Update the Scatter Plots and Horizontal Lines
+ fig.add_trace(
+ go.Scatter(x=list(range(len(dca_spreads))), y=dca_spreads, name='Spread (%)',
+ mode='lines+markers',
+ line=dict(width=3, color=tech_colors['spread'])), secondary_y=False)
+ fig.add_trace(
+ go.Scatter(x=list(range(len(break_even_values))), y=break_even_values, name='Break Even (%)',
+ mode='lines+markers',
+ line=dict(width=3, color=tech_colors['break_even'])), secondary_y=False)
+ fig.add_trace(go.Scatter(x=list(range(len(take_profit_values))), y=take_profit_values, name='Take Profit (%)',
+ mode='lines+markers', line=dict(width=3, color=tech_colors['take_profit'])),
+ secondary_y=False)
+
+ # Add the new Bar Plot for Cumulative Unrealized PNL
+ fig.add_trace(go.Bar(
+ x=list(range(len(cum_unrealized_pnl))),
+ y=cum_unrealized_pnl,
+ text=[f"{pnl:.2f}" for pnl in cum_unrealized_pnl],
+ textposition='auto',
+ textfont=dict(color='white', size=12),
+ name='Cum Unrealized PNL',
+ marker=dict(color='#FFA07A', opacity=0.6) # Light Salmon color, adjust as needed
+ ), secondary_y=True)
+
+ fig.add_trace(go.Bar(
+ x=list(range(len(dca_order_amounts))),
+ y=dca_order_amounts,
+ text=[f"{amt:.2f}" for amt in dca_order_amounts], # List comprehension to format text labels
+ textposition='auto',
+ textfont=dict(
+ color='white',
+ size=12
+ ),
+ name='Order Amount',
+ marker=dict(color=tech_colors['order_amount'], opacity=0.5),
+ ), secondary_y=True)
+
+ # Modify the Bar Plot for Accumulated Amount
+ fig.add_trace(go.Bar(
+ x=list(range(len(accumulated_amount))),
+ y=accumulated_amount,
+ text=[f"{amt:.2f}" for amt in accumulated_amount], # List comprehension to format text labels
+ textposition='auto',
+ textfont=dict(
+ color='white',
+ size=12
+ ),
+ name='Cum Amount',
+ marker=dict(color=tech_colors['cum_amount'], opacity=0.5),
+ ), secondary_y=True)
+
+ # Add Horizontal Lines for Last Breakeven Price and Stop Loss Level
+ last_break_even = break_even_values[-1]
+ stop_loss_value = last_break_even + dca_inputs["stop_loss"] * 100
+ # Horizontal Lines for Last Breakeven and Stop Loss
+ fig.add_hline(y=last_break_even, line_dash="dash", annotation_text=f"Global Break Even: {last_break_even:.2f} (%)",
+ annotation_position="top left", line_color=tech_colors['break_even'])
+ fig.add_hline(y=stop_loss_value, line_dash="dash", annotation_text=f"Stop Loss: {stop_loss_value:.2f} (%)",
+ annotation_position="bottom right", line_color=tech_colors['stop_loss'])
+
+ # Update Annotations for Spread and Break Even
+ for i, (spread, be_value, tp_value) in enumerate(
+ zip(dca_spreads, break_even_values, take_profit_values)):
+ fig.add_annotation(x=i, y=spread, text=f"{spread:.2f}%", showarrow=True, arrowhead=1, yshift=10, xshift=-2,
+ font=dict(color=tech_colors['spread']))
+ fig.add_annotation(x=i, y=be_value, text=f"{be_value:.2f}%", showarrow=True, arrowhead=1, yshift=5, xshift=-2,
+ font=dict(color=tech_colors['break_even']))
+ fig.add_annotation(x=i, y=tp_value, text=f"{tp_value:.2f}%", showarrow=True, arrowhead=1, yshift=10, xshift=-2,
+ font=dict(color=tech_colors['take_profit']))
+ # Update Layout with a Dark Theme
+ fig.update_layout(
+ title="Spread, Accumulated Amount, Break Even, and Take Profit by Order Level",
+ xaxis_title="Order Level",
+ yaxis_title="Spread (%)",
+ yaxis2_title="Amount (Quote)",
+ height=800,
+ width=1800,
+ plot_bgcolor='rgba(0, 0, 0, 0)', # Transparent background
+ paper_bgcolor='rgba(0, 0, 0, 0.1)', # Lighter shade for the paper
+ font=dict(color='white') # Font color
+ )
+
+ # Calculate metrics
+ dca_max_loss = dca_amount * dca_inputs["stop_loss"]
+ profit_per_level = [cum_amount * dca_inputs["take_profit"] for cum_amount in accumulated_amount]
+ loots_to_recover = [dca_max_loss / profit for profit in profit_per_level]
+
+ # Define a consistent annotation size and maximum value for the secondary y-axis
+ circle_text = "β" # Unicode character for a circle
+ max_secondary_value = max(max(accumulated_amount), max(dca_order_amounts),
+ max(cum_unrealized_pnl)) # Adjust based on your secondary y-axis data
+
+ # Determine an appropriate y-offset for annotations
+ y_offset_secondary = max_secondary_value * 0.1 # Adjusts the height relative to the maximum value on the secondary y-axis
+
+ # Add annotations to the Plotly figure for the secondary y-axis
+ for i, loot in enumerate(loots_to_recover):
+ fig.add_annotation(
+ x=i,
+ y=max_secondary_value + y_offset_secondary, # Position above the maximum value using the offset
+ text=f"{circle_text}
LTR: {round(loot, 2)}", # Circle symbol and loot value in separate lines
+ showarrow=False,
+ font=dict(size=16, color='purple'),
+ xanchor="center", # Centers the text above the x coordinate
+ yanchor="bottom", # Anchors the text at its bottom to avoid overlapping
+ align="center",
+ yref="y2" # Reference the secondary y-axis
+ )
+ # Add Max Loss Metric as an Annotation
+ dca_max_loss_annotation_text = f"DCA Max Loss (Quote): {dca_max_loss:.2f}"
+ fig.add_annotation(
+ x=max(len(dca_inputs["dca_spreads"]), len(break_even_values)) - 1, # Positioning the annotation to the right
+ text=dca_max_loss_annotation_text,
+ showarrow=False,
+ font=dict(size=20, color='white'),
+ bgcolor='red', # Red background for emphasis
+ xanchor="left",
+ yanchor="top",
+ yref="y2" # Reference the secondary y-axis
+ )
+ return fig
diff --git a/frontend/visualization/executors.py b/frontend/visualization/executors.py
new file mode 100644
index 0000000..b94cff7
--- /dev/null
+++ b/frontend/visualization/executors.py
@@ -0,0 +1,26 @@
+import plotly.graph_objects as go
+import pandas as pd
+from decimal import Decimal
+
+from hummingbot.connector.connector_base import TradeType
+
+
+def add_executors_trace(fig, executors, row, col):
+ for executor in executors:
+ entry_time = pd.to_datetime(executor.timestamp, unit='s')
+ entry_price = executor.custom_info["current_position_average_price"]
+ exit_time = pd.to_datetime(executor.close_timestamp, unit='s')
+ exit_price = executor.custom_info["close_price"]
+ name = "Buy Executor" if executor.config.side == TradeType.BUY else "Sell Executor"
+
+ if executor.filled_amount_quote == 0:
+ fig.add_trace(go.Scatter(x=[entry_time, exit_time], y=[entry_price, entry_price], mode='lines',
+ line=dict(color='grey', width=2, dash="dash"), name=name), row=row, col=col)
+ else:
+ if executor.net_pnl_quote > Decimal(0):
+ fig.add_trace(go.Scatter(x=[entry_time, exit_time], y=[entry_price, exit_price], mode='lines',
+ line=dict(color='green', width=3), name=name), row=row, col=col)
+ else:
+ fig.add_trace(go.Scatter(x=[entry_time, exit_time], y=[entry_price, exit_price], mode='lines',
+ line=dict(color='red', width=3), name=name), row=row, col=col)
+ return fig
diff --git a/frontend/visualization/executors_distribution.py b/frontend/visualization/executors_distribution.py
new file mode 100644
index 0000000..d5d9f81
--- /dev/null
+++ b/frontend/visualization/executors_distribution.py
@@ -0,0 +1,92 @@
+import numpy as np
+import plotly.graph_objects as go
+import frontend.visualization.theme as theme
+
+
+def create_executors_distribution_traces(buy_spreads, sell_spreads, buy_amounts_pct, sell_amounts_pct, total_amount_quote):
+ colors = theme.get_color_scheme()
+
+ buy_spread_distributions = [spread * 100 for spread in buy_spreads]
+ sell_spread_distributions = [spread * 100 for spread in sell_spreads]
+ buy_order_amounts_quote = [amount * total_amount_quote for amount in buy_amounts_pct]
+ sell_order_amounts_quote = [amount * total_amount_quote for amount in sell_amounts_pct]
+ buy_order_levels = len(buy_spread_distributions)
+ sell_order_levels = len(sell_spread_distributions)
+
+ # Calculate total volumes
+ total_buy_volume = sum(buy_order_amounts_quote)
+ total_sell_volume = sum(sell_order_amounts_quote)
+
+ # Create the figure with a dark theme and secondary y-axis
+ fig = go.Figure()
+
+ # Buy orders on the negative side of x-axis
+ fig.add_trace(go.Bar(
+ x=[-dist for dist in buy_spread_distributions],
+ y=buy_order_amounts_quote,
+ name='Buy Orders',
+ marker_color=colors['buy'],
+ width=[0.2] * buy_order_levels # Adjust the width of the bars as needed
+ ))
+
+ # Sell orders on the positive side of x-axis
+ fig.add_trace(go.Bar(
+ x=sell_spread_distributions,
+ y=sell_order_amounts_quote,
+ name='Sell Orders',
+ marker_color=colors['sell'],
+ width=[0.2] * sell_order_levels # Adjust the width of the bars as needed
+ ))
+
+ # Add annotations for buy orders
+ for i, value in enumerate(buy_order_amounts_quote):
+ fig.add_annotation(
+ x=-buy_spread_distributions[i],
+ y=value + 0.03 * max(buy_order_amounts_quote), # Offset the text slightly above the bar
+ text=str(round(value, 2)),
+ showarrow=False,
+ font=dict(color=colors['buy'], size=10)
+ )
+
+ # Add annotations for sell orders
+ for i, value in enumerate(sell_order_amounts_quote):
+ fig.add_annotation(
+ x=sell_spread_distributions[i],
+ y=value + 0.03 * max(sell_order_amounts_quote), # Offset the text slightly above the bar
+ text=str(round(value, 2)),
+ showarrow=False,
+ font=dict(color=colors['sell'], size=10)
+ )
+
+ max_y = max(max(buy_order_amounts_quote), max(sell_order_amounts_quote))
+ # Add annotations for total volumes
+ fig.add_annotation(
+ x=-np.mean(buy_spread_distributions),
+ y=max_y,
+ text=f'Total Buy\n{round(total_buy_volume, 2)}',
+ showarrow=False,
+ font=dict(color=colors['buy'], size=12, family="Arial Black"),
+ align='center'
+ )
+
+ fig.add_annotation(
+ x=np.mean(sell_spread_distributions),
+ y=max_y,
+ text=f'Total Sell\n{round(total_sell_volume, 2)}',
+ showarrow=False,
+ font=dict(color=colors['sell'], size=12, family="Arial Black"),
+ align='center'
+ )
+
+ # Apply the theme layout
+ layout_settings = theme.get_default_layout("Market Maker Order Distribution")
+ fig.update_layout(**layout_settings)
+ fig.update_layout(
+ xaxis_title="Spread (%)",
+ yaxis_title="Order Amount (Quote)",
+ bargap=0.1, # Adjust the gap between the bars
+ barmode='relative', # Stack the bars on top of each other
+ showlegend=True,
+ height=600
+ )
+ return fig
diff --git a/frontend/visualization/indicators.py b/frontend/visualization/indicators.py
new file mode 100644
index 0000000..e5965a4
--- /dev/null
+++ b/frontend/visualization/indicators.py
@@ -0,0 +1,82 @@
+import pandas as pd
+import pandas_ta as ta # noqa: F401
+import plotly.graph_objects as go
+
+from frontend.visualization import theme
+
+
+def get_bbands_traces(df, bb_length, bb_std):
+ tech_colors = theme.get_color_scheme()
+ df.ta.bbands(length=bb_length, std=bb_std, append=True)
+ bb_lower = f'BBL_{bb_length}_{bb_std}'
+ bb_middle = f'BBM_{bb_length}_{bb_std}'
+ bb_upper = f'BBU_{bb_length}_{bb_std}'
+ traces = [
+ go.Scatter(x=df.index, y=df[bb_upper], line=dict(color=tech_colors['upper_band']),
+ name='Upper Band'),
+ go.Scatter(x=df.index, y=df[bb_middle], line=dict(color=tech_colors['middle_band']),
+ name='Middle Band'),
+ go.Scatter(x=df.index, y=df[bb_lower], line=dict(color=tech_colors['lower_band']),
+ name='Lower Band'),
+ ]
+ return traces
+
+
+def get_volume_trace(df):
+ df.index = pd.to_datetime(df.timestamp, unit='s')
+ return go.Bar(x=df.index, y=df['volume'], name="Volume", marker_color=theme.get_color_scheme()["volume"], opacity=0.7)
+
+def get_macd_traces(df, macd_fast, macd_slow, macd_signal):
+ tech_colors = theme.get_color_scheme()
+ df.ta.macd(fast=macd_fast, slow=macd_slow, signal=macd_signal, append=True)
+ macd = f'MACD_{macd_fast}_{macd_slow}_{macd_signal}'
+ macd_s = f'MACDs_{macd_fast}_{macd_slow}_{macd_signal}'
+ macd_hist = f'MACDh_{macd_fast}_{macd_slow}_{macd_signal}'
+ traces = [
+ go.Scatter(x=df.index, y=df[macd], line=dict(color=tech_colors['macd_line']),
+ name='MACD Line'),
+ go.Scatter(x=df.index, y=df[macd_s], line=dict(color=tech_colors['macd_signal']),
+ name='MACD Signal'),
+ go.Bar(x=df.index, y=df[macd_hist], name='MACD Histogram',
+ marker_color=df[f"MACDh_{macd_fast}_{macd_slow}_{macd_signal}"].apply(lambda x: '#FF6347' if x < 0 else '#32CD32'))
+ ]
+ return traces
+
+
+def get_supertrend_traces(df, length, multiplier):
+ tech_colors = theme.get_color_scheme()
+ df.ta.supertrend(length=length, multiplier=multiplier, append=True)
+ supertrend_d = f'SUPERTd_{length}_{multiplier}'
+ supertrend = f'SUPERT_{length}_{multiplier}'
+ df = df[df[supertrend] > 0]
+
+ # Create segments for line with different colors
+ segments = []
+ current_segment = {"x": [], "y": [], "color": None}
+
+ for i in range(len(df)):
+ if i == 0 or df[supertrend_d].iloc[i] == df[supertrend_d].iloc[i - 1]:
+ current_segment["x"].append(df.index[i])
+ current_segment["y"].append(df[supertrend].iloc[i])
+ current_segment["color"] = tech_colors['buy'] if df[supertrend_d].iloc[i] == 1 else tech_colors['sell']
+ else:
+ segments.append(current_segment)
+ current_segment = {"x": [df.index[i - 1], df.index[i]],
+ "y": [df[supertrend].iloc[i - 1], df[supertrend].iloc[i]],
+ "color": tech_colors['buy'] if df[supertrend_d].iloc[i] == 1 else tech_colors['sell']}
+
+ segments.append(current_segment)
+
+ # Create traces from segments
+ traces = [
+ go.Scatter(
+ x=segment["x"],
+ y=segment["y"],
+ mode='lines',
+ line=dict(color=segment["color"], width=2),
+ name='SuperTrend'
+ ) for segment in segments
+ ]
+
+ return traces
+
diff --git a/frontend/visualization/pnl.py b/frontend/visualization/pnl.py
new file mode 100644
index 0000000..488f75e
--- /dev/null
+++ b/frontend/visualization/pnl.py
@@ -0,0 +1,15 @@
+import plotly.graph_objects as go
+import numpy as np
+import pandas as pd
+
+
+def get_pnl_trace(executors):
+ pnl = [e.net_pnl_quote for e in executors]
+ cum_pnl = np.cumsum(pnl)
+ return go.Scatter(
+ x=pd.to_datetime([e.close_timestamp for e in executors], unit="s"),
+ y=cum_pnl,
+ mode='lines',
+ line=dict(color='gold', width=2, dash="dash"),
+ name='Cumulative PNL'
+ )
diff --git a/frontend/visualization/signals.py b/frontend/visualization/signals.py
new file mode 100644
index 0000000..1cbec29
--- /dev/null
+++ b/frontend/visualization/signals.py
@@ -0,0 +1,57 @@
+from frontend.visualization import theme
+import plotly.graph_objects as go
+import pandas_ta as ta # noqa: F401
+
+
+def get_signal_traces(buy_signals, sell_signals):
+ tech_colors = theme.get_color_scheme()
+ traces = [
+ go.Scatter(x=buy_signals.index, y=buy_signals['close'], mode='markers',
+ marker=dict(color=tech_colors['buy_signal'], size=10, symbol='triangle-up'),
+ name='Buy Signal'),
+ go.Scatter(x=sell_signals.index, y=sell_signals['close'], mode='markers',
+ marker=dict(color=tech_colors['sell_signal'], size=10, symbol='triangle-down'),
+ name='Sell Signal')
+ ]
+ return traces
+
+def get_bollinger_v1_signal_traces(df, bb_length, bb_std, bb_long_threshold, bb_short_threshold):
+ # Add Bollinger Bands
+ candles = df.copy()
+ candles.ta.bbands(length=bb_length, std=bb_std, append=True)
+
+ # Generate conditions
+ buy_signals = candles[candles[f"BBP_{bb_length}_{bb_std}"] < bb_long_threshold]
+ sell_signals = candles[candles[f"BBP_{bb_length}_{bb_std}"] > bb_short_threshold]
+
+ return get_signal_traces(buy_signals, sell_signals)
+
+
+def get_macdbb_v1_signal_traces(df, bb_length, bb_std, bb_long_threshold, bb_short_threshold, macd_fast, macd_slow,
+ macd_signal):
+ tech_colors = theme.get_color_scheme()
+ # Add Bollinger Bands
+ df.ta.bbands(length=bb_length, std=bb_std, append=True)
+ # Add MACD
+ df.ta.macd(fast=macd_fast, slow=macd_slow, signal=macd_signal, append=True)
+ # Decision Logic
+ bbp = df[f"BBP_{bb_length}_{bb_std}"]
+ macdh = df[f"MACDh_{macd_fast}_{macd_slow}_{macd_signal}"]
+ macd = df[f"MACD_{macd_fast}_{macd_slow}_{macd_signal}"]
+
+ buy_signals = df[(bbp < bb_long_threshold) & (macdh > 0) & (macd < 0)]
+ sell_signals = df[(bbp > bb_short_threshold) & (macdh < 0) & (macd > 0)]
+
+ return get_signal_traces(buy_signals, sell_signals)
+
+
+def get_supertrend_v1_signal_traces(df, length, multiplier, percentage_threshold):
+ # Add indicators
+ df.ta.supertrend(length=length, multiplier=multiplier, append=True)
+ df["percentage_distance"] = abs(df["close"] - df[f"SUPERT_{length}_{multiplier}"]) / df["close"]
+
+ # Generate long and short conditions
+ buy_signals = df[(df[f"SUPERTd_{length}_{multiplier}"] == 1) & (df["percentage_distance"] < percentage_threshold)]
+ sell_signals = df[(df[f"SUPERTd_{length}_{multiplier}"] == -1) & (df["percentage_distance"] < percentage_threshold)]
+
+ return get_signal_traces(buy_signals, sell_signals)
diff --git a/frontend/visualization/theme.py b/frontend/visualization/theme.py
new file mode 100644
index 0000000..5f66da0
--- /dev/null
+++ b/frontend/visualization/theme.py
@@ -0,0 +1,41 @@
+def get_default_layout(title=None, height=800, width=1800):
+ layout = {
+ "template": "plotly_dark",
+ "plot_bgcolor": 'rgba(0, 0, 0, 0)', # Transparent background
+ "paper_bgcolor": 'rgba(0, 0, 0, 0.1)', # Lighter shade for the paper
+ "font": {"color": 'white', "size": 12}, # Consistent font color and size
+ "height": height,
+ "width": width,
+ "margin": {"l": 20, "r": 20, "t": 50, "b": 20},
+ "xaxis_rangeslider_visible": False,
+ "hovermode": "x unified",
+ "showlegend": False,
+ }
+ if title:
+ layout["title"] = title
+ return layout
+
+
+def get_color_scheme():
+ return {
+ 'upper_band': '#4682B4',
+ 'middle_band': '#FFD700',
+ 'lower_band': '#32CD32',
+ 'buy_signal': '#1E90FF',
+ 'sell_signal': '#FF0000',
+ 'buy': '#32CD32', # Green for buy orders
+ 'sell': '#FF6347', # Tomato red for sell orders
+ 'macd_line': '#FFA500', # Orange
+ 'macd_signal': '#800080', # Purple
+ 'macd_histogram_positive': '#32CD32', # Green
+ 'macd_histogram_negative': '#FF6347', # Red
+ 'spread': '#00BFFF', # Deep Sky Blue
+ 'break_even': '#FFD700', # Gold
+ 'take_profit': '#32CD32', # Green
+ 'order_amount': '#1E90FF', # Dodger Blue
+ 'cum_amount': '#4682B4', # Steel Blue
+ 'stop_loss': '#FF0000', # Red
+ 'cum_unrealized_pnl': '#FFA07A', # Light Salmon
+ 'volume': '#FFD700', # Gold
+ 'price': '#00008B', # Dark Blue
+ }
diff --git a/frontend/visualization/utils.py b/frontend/visualization/utils.py
new file mode 100644
index 0000000..8c95b85
--- /dev/null
+++ b/frontend/visualization/utils.py
@@ -0,0 +1,3 @@
+def add_traces_to_fig(fig, traces, row=1, col=1):
+ for trace in traces:
+ fig.add_trace(trace, row=row, col=col)
\ No newline at end of file
diff --git a/helpers/add_authemail.py b/helpers/add_authemail.py
deleted file mode 100755
index a7c6897..0000000
--- a/helpers/add_authemail.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import streamlit_authenticator as stauth
-from ruamel.yaml import YAML
-import os
-
-yaml = YAML(typ='safe', pure=True)
-
-# enter email address
-new_email = input("Enter dashboard email >> ")
-
-# if user enter no email address, exit setup!
-if len(new_email) == 0:
- print("\nNo email added, please try again!\n")
- exit()
-
-# load the YAML file
-yaml_file = "../credentials.yml"
-with open(yaml_file, "r") as file:
- data = yaml.load(file)
-
-# append the email address to credentials.yml
-data["preauthorized"]["emails"].append(new_email)
-
-# write the updated data back to the file
-with open(yaml_file, "w") as file:
- yaml.dump(data, file)
-
-print("Email has been successfully added!")
diff --git a/helpers/edit_authadmin_password.py b/helpers/edit_authadmin_password.py
deleted file mode 100644
index 35113e2..0000000
--- a/helpers/edit_authadmin_password.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import streamlit_authenticator as stauth
-from ruamel.yaml import YAML
-import os
-
-yaml = YAML(typ='safe', pure=True)
-
-# enter admin password or use default t3st01
-new_password = input("Enter dashboard password >> ")
-new_password = new_password or "t3st01"
-
-# extract the hash password from the List
-hash_password = stauth.Hasher([new_password]).generate()[0]
-
-# load the YAML file
-yaml_file = "../credentials.yml"
-with open(yaml_file, "r") as file:
- data = yaml.load(file)
-
-# update the admin password on credentials.yml
-data["credentials"]["usernames"]["admin"]["password"] = hash_password
-
-# write the updated data back to the file
-with open(yaml_file, "w") as file:
- yaml.dump(data, file)
-
-print("Admin password has been updated! ")
diff --git a/hummingbot_files/bots/.gitignore b/hummingbot_files/bots/.gitignore
deleted file mode 100644
index 359db62..0000000
--- a/hummingbot_files/bots/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/hummingbot*
\ No newline at end of file
diff --git a/hummingbot_files/bots/data_downloader/conf/.password_verification b/hummingbot_files/bots/data_downloader/conf/.password_verification
deleted file mode 100644
index b8c7618..0000000
--- a/hummingbot_files/bots/data_downloader/conf/.password_verification
+++ /dev/null
@@ -1 +0,0 @@
-7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223864336365306436393461623131396334363135663935366464653839363063227d2c202263697068657274657874223a20223836333266323430613563306131623665353664222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226566373330376531636464373964376132303338323534656139343433663930227d2c20226d6163223a202266393439383534613530633138363633386363353962336133363665633962353333386633613964373266636635343066313034333361353431636232306438227d2c202276657273696f6e223a20337d
\ No newline at end of file
diff --git a/hummingbot_files/bots/data_downloader/conf/conf_client.yml b/hummingbot_files/bots/data_downloader/conf/conf_client.yml
deleted file mode 100644
index ab2880c..0000000
--- a/hummingbot_files/bots/data_downloader/conf/conf_client.yml
+++ /dev/null
@@ -1,199 +0,0 @@
-####################################
-### client_config_map config ###
-####################################
-
-instance_id: 039758736d451914503a45ff596e168902d62557
-
-log_level: INFO
-
-debug_console: false
-
-strategy_report_interval: 900.0
-
-logger_override_whitelist:
-- hummingbot.strategy.arbitrage
-- hummingbot.strategy.cross_exchange_market_making
-- conf
-
-log_file_path: /home/hummingbot/logs
-
-kill_switch_mode: {}
-
-# What to auto-fill in the prompt after each import command (start/config)
-autofill_import: disabled
-
-telegram_mode: {}
-
-# MQTT Bridge configuration.
-mqtt_bridge:
- mqtt_host: localhost
- mqtt_port: 1883
- mqtt_username: ''
- mqtt_password: ''
- mqtt_namespace: hbot
- mqtt_ssl: false
- mqtt_logger: true
- mqtt_notifier: true
- mqtt_commands: true
- mqtt_events: true
- mqtt_external_events: true
- mqtt_autostart: true
-
-# Error log sharing
-send_error_logs: true
-
-# Can store the previous strategy ran for quick retrieval.
-previous_strategy: null
-
-# Advanced database options, currently supports SQLAlchemy's included dialects
-# Reference: https://docs.sqlalchemy.org/en/13/dialects/
-# To use an instance of SQLite DB the required configuration is
-# db_engine: sqlite
-# To use a DBMS the required configuration is
-# db_host: 127.0.0.1
-# db_port: 3306
-# db_username: username
-# db_password: password
-# db_name: dbname
-db_mode:
- db_engine: sqlite
-
-pmm_script_mode: {}
-
-# Balance Limit Configurations
-# e.g. Setting USDT and BTC limits on Binance.
-# balance_asset_limit:
-# binance:
-# BTC: 0.1
-# USDT: 1000
-balance_asset_limit:
- bybit_testnet: {}
- lbank: {}
- binance_us: {}
- crypto_com: {}
- ascend_ex_paper_trade: {}
- hotbit: {}
- gate_io_paper_trade: {}
- bitmex_testnet: {}
- ndax_testnet: {}
- huobi: {}
- probit_kr: {}
- altmarkets: {}
- hitbtc: {}
- foxbit: {}
- ascend_ex: {}
- binance: {}
- okx: {}
- ciex: {}
- bitmex: {}
- bitfinex: {}
- probit: {}
- kraken: {}
- kucoin: {}
- bitmart: {}
- bybit: {}
- bittrex: {}
- btc_markets: {}
- mock_paper_exchange: {}
- kucoin_paper_trade: {}
- ndax: {}
- loopring: {}
- mexc: {}
- whitebit: {}
- coinbase_pro: {}
- binance_paper_trade: {}
- gate_io: {}
-
-# Fixed gas price (in Gwei) for Ethereum transactions
-manual_gas_price: 50.0
-
-# Gateway API Configurations
-# default host to only use localhost
-# Port need to match the final installation port for Gateway
-gateway:
- gateway_api_host: localhost
- gateway_api_port: '15888'
-
-certs_path: /home/hummingbot/certs
-
-# Whether to enable aggregated order and trade data collection
-anonymized_metrics_mode:
- anonymized_metrics_interval_min: 15.0
-
-# Command Shortcuts
-# Define abbreviations for often used commands
-# or batch grouped commands together
-command_shortcuts:
-- command: spreads
- help: Set bid and ask spread
- arguments:
- - Bid Spread
- - Ask Spread
- output:
- - config bid_spread $1
- - config ask_spread $2
-
-# A source for rate oracle, currently ascend_ex, binance, coin_gecko, coin_cap, kucoin, gate_io
-rate_oracle_source:
- name: binance
-
-# A universal token which to display tokens values in, e.g. USD,EUR,BTC
-global_token:
- global_token_name: USD
- global_token_symbol: $
-
-# Percentage of API rate limits (on any exchange and any end point) allocated to this bot instance.
-# Enter 50 to indicate 50%. E.g. if the API rate limit is 100 calls per second, and you allocate
-# 50% to this setting, the bot will have a maximum (limit) of 50 calls per second
-rate_limits_share_pct: 100.0
-
-commands_timeout:
- create_command_timeout: 10.0
- other_commands_timeout: 30.0
-
-# Tabulate table format style (https://github.com/astanin/python-tabulate#table-format)
-tables_format: psql
-
-paper_trade:
- paper_trade_exchanges:
- - binance
- - kucoin
- - ascend_ex
- - gate_io
- paper_trade_account_balance:
- BTC: 1.0
- USDT: 1000.0
- ONE: 1000.0
- USDQ: 1000.0
- TUSD: 1000.0
- ETH: 10.0
- WETH: 10.0
- USDC: 1000.0
- DAI: 1000.0
-
-color:
- top_pane: '#000000'
- bottom_pane: '#000000'
- output_pane: '#262626'
- input_pane: '#1C1C1C'
- logs_pane: '#121212'
- terminal_primary: '#5FFFD7'
- primary_label: '#5FFFD7'
- secondary_label: '#FFFFFF'
- success_label: '#5FFFD7'
- warning_label: '#FFFF00'
- info_label: '#5FD7FF'
- error_label: '#FF0000'
- gold_label: '#FFD700'
- silver_label: '#C0C0C0'
- bronze_label: '#CD7F32'
-
-# The tick size is the frequency with which the clock notifies the time iterators by calling the
-# c_tick() method, that means for example that if the tick size is 1, the logic of the strategy
-# will run every second.
-tick_size: 1.0
-
-market_data_collection:
- market_data_collection_enabled: true
- market_data_collection_interval: 60
- market_data_collection_depth: 20
diff --git a/hummingbot_files/bots/data_downloader/conf/conf_fee_overrides.yml b/hummingbot_files/bots/data_downloader/conf/conf_fee_overrides.yml
deleted file mode 100644
index f295650..0000000
--- a/hummingbot_files/bots/data_downloader/conf/conf_fee_overrides.yml
+++ /dev/null
@@ -1,340 +0,0 @@
-########################################
-### Fee overrides configurations ###
-########################################
-
-# For more detailed information: https://docs.hummingbot.io
-template_version: 14
-
-# Example of the fields that can be specified to override the `TradeFeeFactory` default settings.
-# If the field is missing or the value is left blank, the default value will be used.
-# The percentage values are specified as 0.1 for 0.1%.
-#
-# [exchange name]_percent_fee_token:
-# [exchange name]_maker_percent_fee:
-# [exchange name]_taker_percent_fee:
-# [exchange name]_buy_percent_fee_deducted_from_returns: # if False, the buy fee is added to the order costs
-# [exchange name]_maker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]])
-# [exchange name]_taker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]])
-
-binance_percent_fee_token: # BNB
-binance_maker_percent_fee: # 0.75
-binance_taker_percent_fee: # 0.75
-binance_buy_percent_fee_deducted_from_returns: # True
-
-# List of supported Exchanges for which the user's data_downloader/conf_fee_override.yml
-# will work. This file currently needs to be in sync with hummingbot list of
-# supported exchanges
-altmarkets_buy_percent_fee_deducted_from_returns:
-altmarkets_maker_fixed_fees:
-altmarkets_maker_percent_fee:
-altmarkets_percent_fee_token:
-altmarkets_taker_fixed_fees:
-altmarkets_taker_percent_fee:
-ascend_ex_buy_percent_fee_deducted_from_returns:
-ascend_ex_maker_fixed_fees:
-ascend_ex_maker_percent_fee:
-ascend_ex_percent_fee_token:
-ascend_ex_taker_fixed_fees:
-ascend_ex_taker_percent_fee:
-binance_maker_fixed_fees:
-binance_perpetual_buy_percent_fee_deducted_from_returns:
-binance_perpetual_maker_fixed_fees:
-binance_perpetual_maker_percent_fee:
-binance_perpetual_percent_fee_token:
-binance_perpetual_taker_fixed_fees:
-binance_perpetual_taker_percent_fee:
-binance_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-binance_perpetual_testnet_maker_fixed_fees:
-binance_perpetual_testnet_maker_percent_fee:
-binance_perpetual_testnet_percent_fee_token:
-binance_perpetual_testnet_taker_fixed_fees:
-binance_perpetual_testnet_taker_percent_fee:
-binance_taker_fixed_fees:
-binance_us_buy_percent_fee_deducted_from_returns:
-binance_us_maker_fixed_fees:
-binance_us_maker_percent_fee:
-binance_us_percent_fee_token:
-binance_us_taker_fixed_fees:
-binance_us_taker_percent_fee:
-bitfinex_buy_percent_fee_deducted_from_returns:
-bitfinex_maker_fixed_fees:
-bitfinex_maker_percent_fee:
-bitfinex_percent_fee_token:
-bitfinex_taker_fixed_fees:
-bitfinex_taker_percent_fee:
-bitmart_buy_percent_fee_deducted_from_returns:
-bitmart_maker_fixed_fees:
-bitmart_maker_percent_fee:
-bitmart_percent_fee_token:
-bitmart_taker_fixed_fees:
-bitmart_taker_percent_fee:
-bittrex_buy_percent_fee_deducted_from_returns:
-bittrex_maker_fixed_fees:
-bittrex_maker_percent_fee:
-bittrex_percent_fee_token:
-bittrex_taker_fixed_fees:
-bittrex_taker_percent_fee:
-btc_markets_percent_fee_token:
-btc_markets_maker_percent_fee:
-btc_markets_taker_percent_fee:
-btc_markets_buy_percent_fee_deducted_from_returns:
-bybit_perpetual_buy_percent_fee_deducted_from_returns:
-bybit_perpetual_maker_fixed_fees:
-bybit_perpetual_maker_percent_fee:
-bybit_perpetual_percent_fee_token:
-bybit_perpetual_taker_fixed_fees:
-bybit_perpetual_taker_percent_fee:
-bybit_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-bybit_perpetual_testnet_maker_fixed_fees:
-bybit_perpetual_testnet_maker_percent_fee:
-bybit_perpetual_testnet_percent_fee_token:
-bybit_perpetual_testnet_taker_fixed_fees:
-bybit_perpetual_testnet_taker_percent_fee:
-coinbase_pro_buy_percent_fee_deducted_from_returns:
-coinbase_pro_maker_fixed_fees:
-coinbase_pro_maker_percent_fee:
-coinbase_pro_percent_fee_token:
-coinbase_pro_taker_fixed_fees:
-coinbase_pro_taker_percent_fee:
-crypto_com_buy_percent_fee_deducted_from_returns:
-crypto_com_maker_fixed_fees:
-crypto_com_maker_percent_fee:
-crypto_com_percent_fee_token:
-crypto_com_taker_fixed_fees:
-crypto_com_taker_percent_fee:
-dydx_perpetual_buy_percent_fee_deducted_from_returns:
-dydx_perpetual_maker_fixed_fees:
-dydx_perpetual_maker_percent_fee:
-dydx_perpetual_percent_fee_token:
-dydx_perpetual_taker_fixed_fees:
-dydx_perpetual_taker_percent_fee:
-gate_io_buy_percent_fee_deducted_from_returns:
-gate_io_maker_fixed_fees:
-gate_io_maker_percent_fee:
-gate_io_percent_fee_token:
-gate_io_taker_fixed_fees:
-gate_io_taker_percent_fee:
-hitbtc_buy_percent_fee_deducted_from_returns:
-hitbtc_maker_fixed_fees:
-hitbtc_maker_percent_fee:
-hitbtc_percent_fee_token:
-hitbtc_taker_fixed_fees:
-hitbtc_taker_percent_fee:
-huobi_buy_percent_fee_deducted_from_returns:
-huobi_maker_fixed_fees:
-huobi_maker_percent_fee:
-huobi_percent_fee_token:
-huobi_taker_fixed_fees:
-huobi_taker_percent_fee:
-kraken_buy_percent_fee_deducted_from_returns:
-kraken_maker_fixed_fees:
-kraken_maker_percent_fee:
-kraken_percent_fee_token:
-kraken_taker_fixed_fees:
-kraken_taker_percent_fee:
-kucoin_buy_percent_fee_deducted_from_returns:
-kucoin_maker_fixed_fees:
-kucoin_maker_percent_fee:
-kucoin_percent_fee_token:
-kucoin_taker_fixed_fees:
-kucoin_taker_percent_fee:
-loopring_buy_percent_fee_deducted_from_returns:
-loopring_maker_fixed_fees:
-loopring_maker_percent_fee:
-loopring_percent_fee_token:
-loopring_taker_fixed_fees:
-loopring_taker_percent_fee:
-mexc_buy_percent_fee_deducted_from_returns:
-mexc_maker_fixed_fees:
-mexc_maker_percent_fee:
-mexc_percent_fee_token:
-mexc_taker_fixed_fees:
-mexc_taker_percent_fee:
-ndax_buy_percent_fee_deducted_from_returns:
-ndax_maker_fixed_fees:
-ndax_maker_percent_fee:
-ndax_percent_fee_token:
-ndax_taker_fixed_fees:
-ndax_taker_percent_fee:
-ndax_testnet_buy_percent_fee_deducted_from_returns:
-ndax_testnet_maker_fixed_fees:
-ndax_testnet_maker_percent_fee:
-ndax_testnet_percent_fee_token:
-ndax_testnet_taker_fixed_fees:
-ndax_testnet_taker_percent_fee:
-okx_buy_percent_fee_deducted_from_returns:
-okx_maker_fixed_fees:
-okx_maker_percent_fee:
-okx_percent_fee_token:
-okx_taker_fixed_fees:
-okx_taker_percent_fee:
-probit_buy_percent_fee_deducted_from_returns:
-probit_kr_buy_percent_fee_deducted_from_returns:
-probit_kr_maker_fixed_fees:
-probit_kr_maker_percent_fee:
-probit_kr_percent_fee_token:
-probit_kr_taker_fixed_fees:
-probit_kr_taker_percent_fee:
-probit_maker_fixed_fees:
-probit_maker_percent_fee:
-probit_percent_fee_token:
-probit_taker_fixed_fees:
-probit_taker_percent_fee:
-bitmex_perpetual_percent_fee_token:
-bitmex_perpetual_maker_percent_fee:
-bitmex_perpetual_taker_percent_fee:
-bitmex_perpetual_buy_percent_fee_deducted_from_returns:
-bitmex_perpetual_maker_fixed_fees:
-bitmex_perpetual_taker_fixed_fees:
-bitmex_perpetual_testnet_percent_fee_token:
-bitmex_perpetual_testnet_maker_percent_fee:
-bitmex_perpetual_testnet_taker_percent_fee:
-bitmex_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-bitmex_perpetual_testnet_maker_fixed_fees:
-bitmex_perpetual_testnet_taker_fixed_fees:
-kucoin_perpetual_percent_fee_token:
-kucoin_perpetual_maker_percent_fee:
-kucoin_perpetual_taker_percent_fee:
-kucoin_perpetual_buy_percent_fee_deducted_from_returns:
-kucoin_perpetual_maker_fixed_fees:
-kucoin_perpetual_taker_fixed_fees:
-kucoin_perpetual_testnet_percent_fee_token:
-kucoin_perpetual_testnet_maker_percent_fee:
-kucoin_perpetual_testnet_taker_percent_fee:
-kucoin_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-kucoin_perpetual_testnet_maker_fixed_fees:
-kucoin_perpetual_testnet_taker_fixed_fees:
-gate_io_perpetual_percent_fee_token:
-gate_io_perpetual_maker_percent_fee:
-gate_io_perpetual_taker_percent_fee:
-gate_io_perpetual_buy_percent_fee_deducted_from_returns:
-gate_io_perpetual_maker_fixed_fees:
-gate_io_perpetual_taker_fixed_fees:
-phemex_perpetual_percent_fee_token:
-phemex_perpetual_maker_percent_fee:
-phemex_perpetual_taker_percent_fee:
-phemex_perpetual_buy_percent_fee_deducted_from_returns:
-phemex_perpetual_maker_fixed_fees:
-phemex_perpetual_taker_fixed_fees:
-phemex_perpetual_testnet_percent_fee_token:
-phemex_perpetual_testnet_maker_percent_fee:
-phemex_perpetual_testnet_taker_percent_fee:
-phemex_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-phemex_perpetual_testnet_maker_fixed_fees:
-phemex_perpetual_testnet_taker_fixed_fees:
-bitget_perpetual_percent_fee_token:
-bitget_perpetual_maker_percent_fee:
-bitget_perpetual_taker_percent_fee:
-bitget_perpetual_buy_percent_fee_deducted_from_returns:
-bitget_perpetual_maker_fixed_fees:
-bitget_perpetual_taker_fixed_fees:
-bit_com_perpetual_percent_fee_token:
-bit_com_perpetual_maker_percent_fee:
-bit_com_perpetual_taker_percent_fee:
-bit_com_perpetual_buy_percent_fee_deducted_from_returns:
-bit_com_perpetual_maker_fixed_fees:
-bit_com_perpetual_taker_fixed_fees:
-bit_com_perpetual_testnet_percent_fee_token:
-bit_com_perpetual_testnet_maker_percent_fee:
-bit_com_perpetual_testnet_taker_percent_fee:
-bit_com_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-bit_com_perpetual_testnet_maker_fixed_fees:
-bit_com_perpetual_testnet_taker_fixed_fees:
-whitebit_percent_fee_token:
-whitebit_maker_percent_fee:
-whitebit_taker_percent_fee:
-whitebit_buy_percent_fee_deducted_from_returns:
-whitebit_maker_fixed_fees:
-whitebit_taker_fixed_fees:
-bitmex_percent_fee_token:
-bitmex_maker_percent_fee:
-bitmex_taker_percent_fee:
-bitmex_buy_percent_fee_deducted_from_returns:
-bitmex_maker_fixed_fees:
-bitmex_taker_fixed_fees:
-bitmex_testnet_percent_fee_token:
-bitmex_testnet_maker_percent_fee:
-bitmex_testnet_taker_percent_fee:
-bitmex_testnet_buy_percent_fee_deducted_from_returns:
-bitmex_testnet_maker_fixed_fees:
-bitmex_testnet_taker_fixed_fees:
-ciex_percent_fee_token:
-ciex_maker_percent_fee:
-ciex_taker_percent_fee:
-ciex_buy_percent_fee_deducted_from_returns:
-ciex_maker_fixed_fees:
-ciex_taker_fixed_fees:
-foxbit_percent_fee_token:
-foxbit_maker_percent_fee:
-foxbit_taker_percent_fee:
-foxbit_buy_percent_fee_deducted_from_returns:
-foxbit_maker_fixed_fees:
-foxbit_taker_fixed_fees:
-lbank_percent_fee_token:
-lbank_maker_percent_fee:
-lbank_taker_percent_fee:
-lbank_buy_percent_fee_deducted_from_returns:
-lbank_maker_fixed_fees:
-lbank_taker_fixed_fees:
-bybit_percent_fee_token:
-bybit_maker_percent_fee:
-bybit_taker_percent_fee:
-bybit_buy_percent_fee_deducted_from_returns:
-bybit_maker_fixed_fees:
-bybit_taker_fixed_fees:
-bybit_testnet_percent_fee_token:
-bybit_testnet_maker_percent_fee:
-bybit_testnet_taker_percent_fee:
-bybit_testnet_buy_percent_fee_deducted_from_returns:
-bybit_testnet_maker_fixed_fees:
-bybit_testnet_taker_fixed_fees:
-hotbit_percent_fee_token:
-hotbit_maker_percent_fee:
-hotbit_taker_percent_fee:
-hotbit_buy_percent_fee_deducted_from_returns:
-hotbit_maker_fixed_fees:
-hotbit_taker_fixed_fees:
-btc_markets_maker_fixed_fees:
-btc_markets_taker_fixed_fees:
-polkadex_percent_fee_token:
-polkadex_maker_percent_fee:
-polkadex_taker_percent_fee:
-polkadex_buy_percent_fee_deducted_from_returns:
-polkadex_maker_fixed_fees:
-polkadex_taker_fixed_fees:
-woo_x_percent_fee_token:
-woo_x_maker_percent_fee:
-woo_x_taker_percent_fee:
-woo_x_buy_percent_fee_deducted_from_returns:
-woo_x_maker_fixed_fees:
-woo_x_taker_fixed_fees:
-woo_x_testnet_percent_fee_token:
-woo_x_testnet_maker_percent_fee:
-woo_x_testnet_taker_percent_fee:
-woo_x_testnet_buy_percent_fee_deducted_from_returns:
-woo_x_testnet_maker_fixed_fees:
-woo_x_testnet_taker_fixed_fees:
-vertex_percent_fee_token:
-vertex_maker_percent_fee:
-vertex_taker_percent_fee:
-vertex_buy_percent_fee_deducted_from_returns:
-vertex_maker_fixed_fees:
-vertex_taker_fixed_fees:
-vertex_testnet_percent_fee_token:
-vertex_testnet_maker_percent_fee:
-vertex_testnet_taker_percent_fee:
-vertex_testnet_buy_percent_fee_deducted_from_returns:
-vertex_testnet_maker_fixed_fees:
-vertex_testnet_taker_fixed_fees:
-injective_v2_percent_fee_token:
-injective_v2_maker_percent_fee:
-injective_v2_taker_percent_fee:
-injective_v2_buy_percent_fee_deducted_from_returns:
-injective_v2_maker_fixed_fees:
-injective_v2_taker_fixed_fees:
-injective_v2_perpetual_percent_fee_token:
-injective_v2_perpetual_maker_percent_fee:
-injective_v2_perpetual_taker_percent_fee:
-injective_v2_perpetual_buy_percent_fee_deducted_from_returns:
-injective_v2_perpetual_maker_fixed_fees:
-injective_v2_perpetual_taker_fixed_fees:
diff --git a/hummingbot_files/bots/data_downloader/conf/hummingbot_logs.yml b/hummingbot_files/bots/data_downloader/conf/hummingbot_logs.yml
deleted file mode 100755
index 8e65271..0000000
--- a/hummingbot_files/bots/data_downloader/conf/hummingbot_logs.yml
+++ /dev/null
@@ -1,83 +0,0 @@
----
-version: 1
-template_version: 12
-
-formatters:
- simple:
- format: "%(asctime)s - %(process)d - %(name)s - %(levelname)s - %(message)s"
-
-handlers:
- console:
- class: hummingbot.logger.cli_handler.CLIHandler
- level: DEBUG
- formatter: simple
- stream: ext://sys.stdout
- console_warning:
- class: hummingbot.logger.cli_handler.CLIHandler
- level: WARNING
- formatter: simple
- stream: ext://sys.stdout
- console_info:
- class: hummingbot.logger.cli_handler.CLIHandler
- level: INFO
- formatter: simple
- stream: ext://sys.stdout
- file_handler:
- class: logging.handlers.TimedRotatingFileHandler
- level: DEBUG
- formatter: simple
- filename: $PROJECT_DIR/logs/logs_$STRATEGY_FILE_PATH.log
- encoding: utf8
- when: "D"
- interval: 1
- backupCount: 7
- "null":
- class: logging.NullHandler
- level: DEBUG
-
-loggers:
- hummingbot.core.utils.eth_gas_station_lookup:
- level: NETWORK
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.logger.log_server_client:
- level: WARNING
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.logger.reporting_proxy_handler:
- level: WARNING
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.strategy:
- level: NETWORK
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.connector:
- level: NETWORK
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.client:
- level: NETWORK
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.core.event.event_reporter:
- level: EVENT_LOG
- propagate: false
- handlers: [file_handler]
- mqtt: false
- conf:
- level: NETWORK
- handlers: ["null"]
- propagate: false
- mqtt: false
-
-root:
- level: INFO
- handlers: [console, file_handler]
- mqtt: true
diff --git a/hummingbot_files/bots/data_downloader/conf_client.yml b/hummingbot_files/bots/data_downloader/conf_client.yml
deleted file mode 100644
index 5bc6b61..0000000
--- a/hummingbot_files/bots/data_downloader/conf_client.yml
+++ /dev/null
@@ -1,194 +0,0 @@
-####################################
-### client_config_map config ###
-####################################
-
-instance_id: e90c0d6f2b1e2d54fa0c0a69612b07174320963b
-
-log_level: INFO
-
-debug_console: false
-
-strategy_report_interval: 900.0
-
-logger_override_whitelist:
-- hummingbot.strategy.arbitrage
-- hummingbot.strategy.cross_exchange_market_making
-- conf
-
-log_file_path: /home/hummingbot/logs
-
-kill_switch_mode: {}
-
-# What to auto-fill in the prompt after each import command (start/config)
-autofill_import: disabled
-
-telegram_mode: {}
-
-# MQTT Bridge configuration.
-mqtt_bridge:
- mqtt_host: localhost
- mqtt_port: 1883
- mqtt_username: ''
- mqtt_password: ''
- mqtt_namespace: hbot
- mqtt_ssl: false
- mqtt_logger: true
- mqtt_notifier: true
- mqtt_commands: true
- mqtt_events: true
- mqtt_external_events: true
- mqtt_autostart: true
-
-# Error log sharing
-send_error_logs: true
-
-# Can store the previous strategy ran for quick retrieval.
-previous_strategy: null
-
-# Advanced database options, currently supports SQLAlchemy's included dialects
-# Reference: https://docs.sqlalchemy.org/en/13/dialects/
-# To use an instance of SQLite DB the required configuration is
-# db_engine: sqlite
-# To use a DBMS the required configuration is
-# db_host: 127.0.0.1
-# db_port: 3306
-# db_username: username
-# db_password: password
-# db_name: dbname
-db_mode:
- db_engine: sqlite
-
-pmm_script_mode: {}
-
-# Balance Limit Configurations
-# e.g. Setting USDT and BTC limits on Binance.
-# balance_asset_limit:
-# binance:
-# BTC: 0.1
-# USDT: 1000
-balance_asset_limit:
- kucoin: {}
- ciex: {}
- ascend_ex_paper_trade: {}
- crypto_com: {}
- mock_paper_exchange: {}
- btc_markets: {}
- bitmart: {}
- hitbtc: {}
- loopring: {}
- mexc: {}
- polkadex: {}
- bybit: {}
- foxbit: {}
- gate_io_paper_trade: {}
- kucoin_paper_trade: {}
- altmarkets: {}
- ascend_ex: {}
- bittrex: {}
- probit_kr: {}
- binance: {}
- bybit_testnet: {}
- okx: {}
- bitmex: {}
- binance_us: {}
- probit: {}
- gate_io: {}
- lbank: {}
- whitebit: {}
- bitmex_testnet: {}
- kraken: {}
- huobi: {}
- binance_paper_trade: {}
- ndax_testnet: {}
- coinbase_pro: {}
- ndax: {}
- bitfinex: {}
-
-# Fixed gas price (in Gwei) for Ethereum transactions
-manual_gas_price: 50.0
-
-# Gateway API Configurations
-# default host to only use localhost
-# Port need to match the final installation port for Gateway
-gateway:
- gateway_api_host: localhost
- gateway_api_port: '15888'
-
-certs_path: /home/hummingbot/certs
-
-# Whether to enable aggregated order and trade data collection
-anonymized_metrics_mode:
- anonymized_metrics_interval_min: 15.0
-
-# Command Shortcuts
-# Define abbreviations for often used commands
-# or batch grouped commands together
-command_shortcuts:
-- command: spreads
- help: Set bid and ask spread
- arguments:
- - Bid Spread
- - Ask Spread
- output:
- - config bid_spread $1
- - config ask_spread $2
-
-# A source for rate oracle, currently ascend_ex, binance, coin_gecko, kucoin, gate_io
-rate_oracle_source:
- name: binance
-
-# A universal token which to display tokens values in, e.g. USD,EUR,BTC
-global_token:
- global_token_name: USD
- global_token_symbol: $
-
-# Percentage of API rate limits (on any exchange and any end point) allocated to this bot instance.
-# Enter 50 to indicate 50%. E.g. if the API rate limit is 100 calls per second, and you allocate
-# 50% to this setting, the bot will have a maximum (limit) of 50 calls per second
-rate_limits_share_pct: 100.0
-
-commands_timeout:
- create_command_timeout: 10.0
- other_commands_timeout: 30.0
-
-# Tabulate table format style (https://github.com/astanin/python-tabulate#table-format)
-tables_format: psql
-
-paper_trade:
- paper_trade_exchanges:
- - binance
- - kucoin
- - ascend_ex
- - gate_io
- paper_trade_account_balance:
- BTC: 1.0
- USDT: 1000.0
- ONE: 1000.0
- USDQ: 1000.0
- TUSD: 1000.0
- ETH: 10.0
- WETH: 10.0
- USDC: 1000.0
- DAI: 1000.0
-
-color:
- top_pane: '#000000'
- bottom_pane: '#000000'
- output_pane: '#262626'
- input_pane: '#1C1C1C'
- logs_pane: '#121212'
- terminal_primary: '#5FFFD7'
- primary_label: '#5FFFD7'
- secondary_label: '#FFFFFF'
- success_label: '#5FFFD7'
- warning_label: '#FFFF00'
- info_label: '#5FD7FF'
- error_label: '#FF0000'
- gold_label: '#FFD700'
- silver_label: '#C0C0C0'
- bronze_label: '#CD7F32'
-
-# The tick size is the frequency with which the clock notifies the time iterators by calling the
-# c_tick() method, that means for example that if the tick size is 1, the logic of the strategy
-# will run every second.
-tick_size: 1.0
diff --git a/hummingbot_files/bots/data_downloader/scripts/download_candles.py b/hummingbot_files/bots/data_downloader/scripts/download_candles.py
deleted file mode 100644
index bfb5d10..0000000
--- a/hummingbot_files/bots/data_downloader/scripts/download_candles.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import os
-from typing import Dict
-
-from hummingbot import data_path
-from hummingbot.client.hummingbot_application import HummingbotApplication
-from hummingbot.connector.connector_base import ConnectorBase
-from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class DownloadCandles(ScriptStrategyBase):
- """
- This script provides an example of how to use the Candles Feed to download and store historical data.
- It downloads 3-minute candles for 3 Binance trading pairs ["APE-USDT", "BTC-USDT", "BNB-USDT"] and stores them in
- CSV files in the /data directory. The script stops after it has downloaded 50,000 max_records records for each pair.
- Is important to notice that the component will fail if all the candles are not available since the idea of it is to
- use it in production based on candles needed to compute technical indicators.
- """
- exchange = os.getenv("EXCHANGE", "binance_perpetual")
- trading_pairs = os.getenv("TRADING_PAIRS", "DODO-BUSD,LTC-USDT").split(",")
- intervals = os.getenv("INTERVALS", "1m,3m,5m,1h").split(",")
- days_to_download = int(os.getenv("DAYS_TO_DOWNLOAD", "3"))
- # we can initialize any trading pair since we only need the candles
- markets = {"binance_paper_trade": {"BTC-USDT"}}
-
- @staticmethod
- def get_max_records(days_to_download: int, interval: str) -> int:
- conversion = {"s": 1 / 60, "m": 1, "h": 60, "d": 1440}
- unit = interval[-1]
- quantity = int(interval[:-1])
- return int(days_to_download * 24 * 60 / (quantity * conversion[unit]))
-
- def __init__(self, connectors: Dict[str, ConnectorBase]):
- super().__init__(connectors)
- combinations = [(trading_pair, interval) for trading_pair in self.trading_pairs for interval in self.intervals]
-
- self.candles = {f"{combinations[0]}_{combinations[1]}": {} for combinations in combinations}
- # we need to initialize the candles for each trading pair
- for combination in combinations:
-
- candle = CandlesFactory.get_candle(CandlesConfig(connector=self.exchange, trading_pair=combination[0], interval=combination[1], max_records=self.get_max_records(self.days_to_download, combination[1])))
- candle.start()
- # we are storing the candles object and the csv path to save the candles
- self.candles[f"{combination[0]}_{combination[1]}"]["candles"] = candle
- self.candles[f"{combination[0]}_{combination[1]}"][
- "csv_path"] = data_path() + f"/candles_{self.exchange}_{combination[0]}_{combination[1]}.csv"
-
- def on_tick(self):
- for trading_pair, candles_info in self.candles.items():
- if not candles_info["candles"].ready:
- self.logger().info(f"Candles not ready yet for {trading_pair}! Missing {candles_info['candles']._candles.maxlen - len(candles_info['candles']._candles)}")
- pass
- else:
- df = candles_info["candles"].candles_df
- df.to_csv(candles_info["csv_path"], index=False)
- if all(candles_info["candles"].ready for candles_info in self.candles.values()):
- HummingbotApplication.main_application().stop()
-
- def on_stop(self):
- for candles_info in self.candles.values():
- candles_info["candles"].stop()
diff --git a/hummingbot_files/compose_files/broker-compose.yml b/hummingbot_files/compose_files/broker-compose.yml
deleted file mode 100644
index e4aad77..0000000
--- a/hummingbot_files/compose_files/broker-compose.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-version: '3.9'
-
-services:
- emqx:
- container_name: hummingbot-broker
- image: emqx:5
- restart: unless-stopped
- environment:
- - EMQX_NAME=emqx
- - EMQX_HOST=node1.emqx.local
- - EMQX_CLUSTER__DISCOVERY_STRATEGY=static
- - EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.local]
- - EMQX_LOADED_PLUGINS="emqx_recon,emqx_retainer,emqx_management,emqx_dashboard"
- volumes:
- - emqx-data:/opt/emqx/data
- - emqx-log:/opt/emqx/log
- - emqx-etc:/opt/emqx/etc
- ports:
- - "1883:1883" # mqtt:tcp
- - "8883:8883" # mqtt:tcp:ssl
- - "8083:8083" # mqtt:ws
- - "8084:8084" # mqtt:ws:ssl
- - "8081:8081" # http:management
- - "18083:18083" # http:dashboard
- - "61613:61613" # web-stomp gateway
- networks:
- emqx-bridge:
- aliases:
- - node1.emqx.local
- healthcheck:
- test: [ "CMD", "/opt/emqx/bin/emqx_ctl", "status" ]
- interval: 5s
- timeout: 25s
- retries: 5
-
-networks:
- emqx-bridge:
- driver: bridge
-
-volumes:
- emqx-data: { }
- emqx-log: { }
- emqx-etc: { }
\ No newline at end of file
diff --git a/hummingbot_files/compose_files/data-downloader-compose.yml b/hummingbot_files/compose_files/data-downloader-compose.yml
deleted file mode 100644
index 468220b..0000000
--- a/hummingbot_files/compose_files/data-downloader-compose.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-version: "3.9"
-services:
- bot:
- container_name: candles_downloader
- image: hummingbot/hummingbot:development
- volumes:
- - "../../data/candles:/home/hummingbot/data"
- - "../bots/data_downloader/conf:/home/hummingbot/conf"
- - "../bots/data_downloader/conf/connectors:/home/hummingbot/conf/connectors"
- - "../bots/data_downloader/scripts:/home/hummingbot/scripts"
- environment:
- - CONFIG_PASSWORD=a
- - CONFIG_FILE_NAME=download_candles.py
- env_file:
- - ../scripts_configs/data_downloader_config.yml
- logging:
- driver: "json-file"
- options:
- max-size: "10m"
- max-file: 5
- tty: true
- stdin_open: true
- network_mode: host
diff --git a/hummingbot_files/controller_configs/.gitignore b/hummingbot_files/controller_configs/.gitignore
deleted file mode 100644
index 42780ec..0000000
--- a/hummingbot_files/controller_configs/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
- *
\ No newline at end of file
diff --git a/hummingbot_files/scripts_configs/data_downloader_config.yml b/hummingbot_files/scripts_configs/data_downloader_config.yml
deleted file mode 100644
index 7fefb6e..0000000
--- a/hummingbot_files/scripts_configs/data_downloader_config.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-DAYS_TO_DOWNLOAD: 30
-EXCHANGE: binance_perpetual
-INTERVALS: 1m,3m,1h
-TRADING_PAIRS: BTC-USDT,ETH-USDT
diff --git a/hummingbot_files/templates/master_bot_conf/conf/.password_verification b/hummingbot_files/templates/master_bot_conf/conf/.password_verification
deleted file mode 100644
index b8c7618..0000000
--- a/hummingbot_files/templates/master_bot_conf/conf/.password_verification
+++ /dev/null
@@ -1 +0,0 @@
-7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223864336365306436393461623131396334363135663935366464653839363063227d2c202263697068657274657874223a20223836333266323430613563306131623665353664222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226566373330376531636464373964376132303338323534656139343433663930227d2c20226d6163223a202266393439383534613530633138363633386363353962336133363665633962353333386633613964373266636635343066313034333361353431636232306438227d2c202276657273696f6e223a20337d
\ No newline at end of file
diff --git a/hummingbot_files/templates/master_bot_conf/conf/conf_client.yml b/hummingbot_files/templates/master_bot_conf/conf/conf_client.yml
deleted file mode 100644
index 9e2b6e1..0000000
--- a/hummingbot_files/templates/master_bot_conf/conf/conf_client.yml
+++ /dev/null
@@ -1,199 +0,0 @@
-####################################
-### client_config_map config ###
-####################################
-
-instance_id: hummingbot-master_bot_conf
-
-log_level: INFO
-
-debug_console: false
-
-strategy_report_interval: 900.0
-
-logger_override_whitelist:
-- hummingbot.strategy.arbitrage
-- hummingbot.strategy.cross_exchange_market_making
-- conf
-
-log_file_path: /home/hummingbot/logs
-
-kill_switch_mode: {}
-
-# What to auto-fill in the prompt after each import command (start/config)
-autofill_import: disabled
-
-telegram_mode: {}
-
-# MQTT Bridge configuration.
-mqtt_bridge:
- mqtt_host: localhost
- mqtt_port: 1883
- mqtt_username: ''
- mqtt_password: ''
- mqtt_namespace: hbot
- mqtt_ssl: false
- mqtt_logger: true
- mqtt_notifier: true
- mqtt_commands: true
- mqtt_events: true
- mqtt_external_events: true
- mqtt_autostart: true
-
-# Error log sharing
-send_error_logs: true
-
-# Can store the previous strategy ran for quick retrieval.
-previous_strategy: null
-
-# Advanced database options, currently supports SQLAlchemy's included dialects
-# Reference: https://docs.sqlalchemy.org/en/13/dialects/
-# To use an instance of SQLite DB the required configuration is
-# db_engine: sqlite
-# To use a DBMS the required configuration is
-# db_host: 127.0.0.1
-# db_port: 3306
-# db_username: username
-# db_password: password
-# db_name: dbname
-db_mode:
- db_engine: sqlite
-
-pmm_script_mode: {}
-
-# Balance Limit Configurations
-# e.g. Setting USDT and BTC limits on Binance.
-# balance_asset_limit:
-# binance:
-# BTC: 0.1
-# USDT: 1000
-balance_asset_limit:
- altmarkets: {}
- ascend_ex: {}
- ascend_ex_paper_trade: {}
- binance: {}
- binance_paper_trade: {}
- binance_us: {}
- bitfinex: {}
- bitmart: {}
- bitmex: {}
- bitmex_testnet: {}
- bittrex: {}
- btc_markets: {}
- bybit: {}
- bybit_testnet: {}
- ciex: {}
- coinbase_pro: {}
- crypto_com: {}
- foxbit: {}
- gate_io: {}
- gate_io_paper_trade: {}
- hitbtc: {}
- hotbit: {}
- huobi: {}
- kraken: {}
- kucoin: {}
- kucoin_paper_trade: {}
- lbank: {}
- loopring: {}
- mexc: {}
- mock_paper_exchange: {}
- ndax: {}
- ndax_testnet: {}
- okx: {}
- probit: {}
- probit_kr: {}
- whitebit: {}
-
-# Fixed gas price (in Gwei) for Ethereum transactions
-manual_gas_price: 50.0
-
-# Gateway API Configurations
-# default host to only use localhost
-# Port need to match the final installation port for Gateway
-gateway:
- gateway_api_host: localhost
- gateway_api_port: '15888'
-
-certs_path: /home/hummingbot/certs
-
-# Whether to enable aggregated order and trade data collection
-anonymized_metrics_mode:
- anonymized_metrics_interval_min: 15.0
-
-# Command Shortcuts
-# Define abbreviations for often used commands
-# or batch grouped commands together
-command_shortcuts:
-- command: spreads
- help: Set bid and ask spread
- arguments:
- - Bid Spread
- - Ask Spread
- output:
- - config bid_spread $1
- - config ask_spread $2
-
-# A source for rate oracle, currently ascend_ex, binance, coin_gecko, coin_cap, kucoin, gate_io
-rate_oracle_source:
- name: binance
-
-# A universal token which to display tokens values in, e.g. USD,EUR,BTC
-global_token:
- global_token_name: USD
- global_token_symbol: $
-
-# Percentage of API rate limits (on any exchange and any end point) allocated to this bot instance.
-# Enter 50 to indicate 50%. E.g. if the API rate limit is 100 calls per second, and you allocate
-# 50% to this setting, the bot will have a maximum (limit) of 50 calls per second
-rate_limits_share_pct: 100.0
-
-commands_timeout:
- create_command_timeout: 10.0
- other_commands_timeout: 30.0
-
-# Tabulate table format style (https://github.com/astanin/python-tabulate#table-format)
-tables_format: psql
-
-paper_trade:
- paper_trade_exchanges:
- - binance
- - kucoin
- - ascend_ex
- - gate_io
- paper_trade_account_balance:
- BTC: 1.0
- DAI: 1000.0
- ETH: 10.0
- ONE: 1000.0
- TUSD: 1000.0
- USDC: 1000.0
- USDQ: 1000.0
- USDT: 1000.0
- WETH: 10.0
-
-color:
- top_pane: '#000000'
- bottom_pane: '#000000'
- output_pane: '#262626'
- input_pane: '#1C1C1C'
- logs_pane: '#121212'
- terminal_primary: '#5FFFD7'
- primary_label: '#5FFFD7'
- secondary_label: '#FFFFFF'
- success_label: '#5FFFD7'
- warning_label: '#FFFF00'
- info_label: '#5FD7FF'
- error_label: '#FF0000'
- gold_label: '#FFD700'
- silver_label: '#C0C0C0'
- bronze_label: '#CD7F32'
-
-# The tick size is the frequency with which the clock notifies the time iterators by calling the
-# c_tick() method, that means for example that if the tick size is 1, the logic of the strategy
-# will run every second.
-tick_size: 1.0
-
-market_data_collection:
- market_data_collection_enabled: true
- market_data_collection_interval: 60
- market_data_collection_depth: 20
diff --git a/hummingbot_files/templates/master_bot_conf/conf/conf_fee_overrides.yml b/hummingbot_files/templates/master_bot_conf/conf/conf_fee_overrides.yml
deleted file mode 100644
index 8648bb5..0000000
--- a/hummingbot_files/templates/master_bot_conf/conf/conf_fee_overrides.yml
+++ /dev/null
@@ -1,340 +0,0 @@
-########################################
-### Fee overrides configurations ###
-########################################
-
-# For more detailed information: https://docs.hummingbot.io
-template_version: 14
-
-# Example of the fields that can be specified to override the `TradeFeeFactory` default settings.
-# If the field is missing or the value is left blank, the default value will be used.
-# The percentage values are specified as 0.1 for 0.1%.
-#
-# [exchange name]_percent_fee_token:
-# [exchange name]_maker_percent_fee:
-# [exchange name]_taker_percent_fee:
-# [exchange name]_buy_percent_fee_deducted_from_returns: # if False, the buy fee is added to the order costs
-# [exchange name]_maker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]])
-# [exchange name]_taker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]])
-
-binance_percent_fee_token: # BNB
-binance_maker_percent_fee: # 0.75
-binance_taker_percent_fee: # 0.75
-binance_buy_percent_fee_deducted_from_returns: # True
-
-# List of supported Exchanges for which the user's data_downloader/conf_fee_override.yml
-# will work. This file currently needs to be in sync with hummingbot list of
-# supported exchanges
-altmarkets_buy_percent_fee_deducted_from_returns:
-altmarkets_maker_fixed_fees:
-altmarkets_maker_percent_fee:
-altmarkets_percent_fee_token:
-altmarkets_taker_fixed_fees:
-altmarkets_taker_percent_fee:
-ascend_ex_buy_percent_fee_deducted_from_returns:
-ascend_ex_maker_fixed_fees:
-ascend_ex_maker_percent_fee:
-ascend_ex_percent_fee_token:
-ascend_ex_taker_fixed_fees:
-ascend_ex_taker_percent_fee:
-binance_maker_fixed_fees:
-binance_perpetual_buy_percent_fee_deducted_from_returns:
-binance_perpetual_maker_fixed_fees:
-binance_perpetual_maker_percent_fee:
-binance_perpetual_percent_fee_token:
-binance_perpetual_taker_fixed_fees:
-binance_perpetual_taker_percent_fee:
-binance_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-binance_perpetual_testnet_maker_fixed_fees:
-binance_perpetual_testnet_maker_percent_fee:
-binance_perpetual_testnet_percent_fee_token:
-binance_perpetual_testnet_taker_fixed_fees:
-binance_perpetual_testnet_taker_percent_fee:
-binance_taker_fixed_fees:
-binance_us_buy_percent_fee_deducted_from_returns:
-binance_us_maker_fixed_fees:
-binance_us_maker_percent_fee:
-binance_us_percent_fee_token:
-binance_us_taker_fixed_fees:
-binance_us_taker_percent_fee:
-bitfinex_buy_percent_fee_deducted_from_returns:
-bitfinex_maker_fixed_fees:
-bitfinex_maker_percent_fee:
-bitfinex_percent_fee_token:
-bitfinex_taker_fixed_fees:
-bitfinex_taker_percent_fee:
-bitmart_buy_percent_fee_deducted_from_returns:
-bitmart_maker_fixed_fees:
-bitmart_maker_percent_fee:
-bitmart_percent_fee_token:
-bitmart_taker_fixed_fees:
-bitmart_taker_percent_fee:
-bittrex_buy_percent_fee_deducted_from_returns:
-bittrex_maker_fixed_fees:
-bittrex_maker_percent_fee:
-bittrex_percent_fee_token:
-bittrex_taker_fixed_fees:
-bittrex_taker_percent_fee:
-btc_markets_percent_fee_token:
-btc_markets_maker_percent_fee:
-btc_markets_taker_percent_fee:
-btc_markets_buy_percent_fee_deducted_from_returns:
-bybit_perpetual_buy_percent_fee_deducted_from_returns:
-bybit_perpetual_maker_fixed_fees:
-bybit_perpetual_maker_percent_fee:
-bybit_perpetual_percent_fee_token:
-bybit_perpetual_taker_fixed_fees:
-bybit_perpetual_taker_percent_fee:
-bybit_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-bybit_perpetual_testnet_maker_fixed_fees:
-bybit_perpetual_testnet_maker_percent_fee:
-bybit_perpetual_testnet_percent_fee_token:
-bybit_perpetual_testnet_taker_fixed_fees:
-bybit_perpetual_testnet_taker_percent_fee:
-coinbase_pro_buy_percent_fee_deducted_from_returns:
-coinbase_pro_maker_fixed_fees:
-coinbase_pro_maker_percent_fee:
-coinbase_pro_percent_fee_token:
-coinbase_pro_taker_fixed_fees:
-coinbase_pro_taker_percent_fee:
-crypto_com_buy_percent_fee_deducted_from_returns:
-crypto_com_maker_fixed_fees:
-crypto_com_maker_percent_fee:
-crypto_com_percent_fee_token:
-crypto_com_taker_fixed_fees:
-crypto_com_taker_percent_fee:
-dydx_perpetual_buy_percent_fee_deducted_from_returns:
-dydx_perpetual_maker_fixed_fees:
-dydx_perpetual_maker_percent_fee:
-dydx_perpetual_percent_fee_token:
-dydx_perpetual_taker_fixed_fees:
-dydx_perpetual_taker_percent_fee:
-gate_io_buy_percent_fee_deducted_from_returns:
-gate_io_maker_fixed_fees:
-gate_io_maker_percent_fee:
-gate_io_percent_fee_token:
-gate_io_taker_fixed_fees:
-gate_io_taker_percent_fee:
-hitbtc_buy_percent_fee_deducted_from_returns:
-hitbtc_maker_fixed_fees:
-hitbtc_maker_percent_fee:
-hitbtc_percent_fee_token:
-hitbtc_taker_fixed_fees:
-hitbtc_taker_percent_fee:
-huobi_buy_percent_fee_deducted_from_returns:
-huobi_maker_fixed_fees:
-huobi_maker_percent_fee:
-huobi_percent_fee_token:
-huobi_taker_fixed_fees:
-huobi_taker_percent_fee:
-kraken_buy_percent_fee_deducted_from_returns:
-kraken_maker_fixed_fees:
-kraken_maker_percent_fee:
-kraken_percent_fee_token:
-kraken_taker_fixed_fees:
-kraken_taker_percent_fee:
-kucoin_buy_percent_fee_deducted_from_returns:
-kucoin_maker_fixed_fees:
-kucoin_maker_percent_fee:
-kucoin_percent_fee_token:
-kucoin_taker_fixed_fees:
-kucoin_taker_percent_fee:
-loopring_buy_percent_fee_deducted_from_returns:
-loopring_maker_fixed_fees:
-loopring_maker_percent_fee:
-loopring_percent_fee_token:
-loopring_taker_fixed_fees:
-loopring_taker_percent_fee:
-mexc_buy_percent_fee_deducted_from_returns:
-mexc_maker_fixed_fees:
-mexc_maker_percent_fee:
-mexc_percent_fee_token:
-mexc_taker_fixed_fees:
-mexc_taker_percent_fee:
-ndax_buy_percent_fee_deducted_from_returns:
-ndax_maker_fixed_fees:
-ndax_maker_percent_fee:
-ndax_percent_fee_token:
-ndax_taker_fixed_fees:
-ndax_taker_percent_fee:
-ndax_testnet_buy_percent_fee_deducted_from_returns:
-ndax_testnet_maker_fixed_fees:
-ndax_testnet_maker_percent_fee:
-ndax_testnet_percent_fee_token:
-ndax_testnet_taker_fixed_fees:
-ndax_testnet_taker_percent_fee:
-okx_buy_percent_fee_deducted_from_returns:
-okx_maker_fixed_fees:
-okx_maker_percent_fee:
-okx_percent_fee_token:
-okx_taker_fixed_fees:
-okx_taker_percent_fee:
-probit_buy_percent_fee_deducted_from_returns:
-probit_kr_buy_percent_fee_deducted_from_returns:
-probit_kr_maker_fixed_fees:
-probit_kr_maker_percent_fee:
-probit_kr_percent_fee_token:
-probit_kr_taker_fixed_fees:
-probit_kr_taker_percent_fee:
-probit_maker_fixed_fees:
-probit_maker_percent_fee:
-probit_percent_fee_token:
-probit_taker_fixed_fees:
-probit_taker_percent_fee:
-bitmex_perpetual_percent_fee_token:
-bitmex_perpetual_maker_percent_fee:
-bitmex_perpetual_taker_percent_fee:
-bitmex_perpetual_buy_percent_fee_deducted_from_returns:
-bitmex_perpetual_maker_fixed_fees:
-bitmex_perpetual_taker_fixed_fees:
-bitmex_perpetual_testnet_percent_fee_token:
-bitmex_perpetual_testnet_maker_percent_fee:
-bitmex_perpetual_testnet_taker_percent_fee:
-bitmex_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-bitmex_perpetual_testnet_maker_fixed_fees:
-bitmex_perpetual_testnet_taker_fixed_fees:
-kucoin_perpetual_percent_fee_token:
-kucoin_perpetual_maker_percent_fee:
-kucoin_perpetual_taker_percent_fee:
-kucoin_perpetual_buy_percent_fee_deducted_from_returns:
-kucoin_perpetual_maker_fixed_fees:
-kucoin_perpetual_taker_fixed_fees:
-kucoin_perpetual_testnet_percent_fee_token:
-kucoin_perpetual_testnet_maker_percent_fee:
-kucoin_perpetual_testnet_taker_percent_fee:
-kucoin_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-kucoin_perpetual_testnet_maker_fixed_fees:
-kucoin_perpetual_testnet_taker_fixed_fees:
-gate_io_perpetual_percent_fee_token:
-gate_io_perpetual_maker_percent_fee:
-gate_io_perpetual_taker_percent_fee:
-gate_io_perpetual_buy_percent_fee_deducted_from_returns:
-gate_io_perpetual_maker_fixed_fees:
-gate_io_perpetual_taker_fixed_fees:
-phemex_perpetual_percent_fee_token:
-phemex_perpetual_maker_percent_fee:
-phemex_perpetual_taker_percent_fee:
-phemex_perpetual_buy_percent_fee_deducted_from_returns:
-phemex_perpetual_maker_fixed_fees:
-phemex_perpetual_taker_fixed_fees:
-phemex_perpetual_testnet_percent_fee_token:
-phemex_perpetual_testnet_maker_percent_fee:
-phemex_perpetual_testnet_taker_percent_fee:
-phemex_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-phemex_perpetual_testnet_maker_fixed_fees:
-phemex_perpetual_testnet_taker_fixed_fees:
-bitget_perpetual_percent_fee_token:
-bitget_perpetual_maker_percent_fee:
-bitget_perpetual_taker_percent_fee:
-bitget_perpetual_buy_percent_fee_deducted_from_returns:
-bitget_perpetual_maker_fixed_fees:
-bitget_perpetual_taker_fixed_fees:
-bit_com_perpetual_percent_fee_token:
-bit_com_perpetual_maker_percent_fee:
-bit_com_perpetual_taker_percent_fee:
-bit_com_perpetual_buy_percent_fee_deducted_from_returns:
-bit_com_perpetual_maker_fixed_fees:
-bit_com_perpetual_taker_fixed_fees:
-bit_com_perpetual_testnet_percent_fee_token:
-bit_com_perpetual_testnet_maker_percent_fee:
-bit_com_perpetual_testnet_taker_percent_fee:
-bit_com_perpetual_testnet_buy_percent_fee_deducted_from_returns:
-bit_com_perpetual_testnet_maker_fixed_fees:
-bit_com_perpetual_testnet_taker_fixed_fees:
-whitebit_percent_fee_token:
-whitebit_maker_percent_fee:
-whitebit_taker_percent_fee:
-whitebit_buy_percent_fee_deducted_from_returns:
-whitebit_maker_fixed_fees:
-whitebit_taker_fixed_fees:
-bitmex_percent_fee_token:
-bitmex_maker_percent_fee:
-bitmex_taker_percent_fee:
-bitmex_buy_percent_fee_deducted_from_returns:
-bitmex_maker_fixed_fees:
-bitmex_taker_fixed_fees:
-bitmex_testnet_percent_fee_token:
-bitmex_testnet_maker_percent_fee:
-bitmex_testnet_taker_percent_fee:
-bitmex_testnet_buy_percent_fee_deducted_from_returns:
-bitmex_testnet_maker_fixed_fees:
-bitmex_testnet_taker_fixed_fees:
-ciex_percent_fee_token:
-ciex_maker_percent_fee:
-ciex_taker_percent_fee:
-ciex_buy_percent_fee_deducted_from_returns:
-ciex_maker_fixed_fees:
-ciex_taker_fixed_fees:
-foxbit_percent_fee_token:
-foxbit_maker_percent_fee:
-foxbit_taker_percent_fee:
-foxbit_buy_percent_fee_deducted_from_returns:
-foxbit_maker_fixed_fees:
-foxbit_taker_fixed_fees:
-lbank_percent_fee_token:
-lbank_maker_percent_fee:
-lbank_taker_percent_fee:
-lbank_buy_percent_fee_deducted_from_returns:
-lbank_maker_fixed_fees:
-lbank_taker_fixed_fees:
-bybit_percent_fee_token:
-bybit_maker_percent_fee:
-bybit_taker_percent_fee:
-bybit_buy_percent_fee_deducted_from_returns:
-bybit_maker_fixed_fees:
-bybit_taker_fixed_fees:
-bybit_testnet_percent_fee_token:
-bybit_testnet_maker_percent_fee:
-bybit_testnet_taker_percent_fee:
-bybit_testnet_buy_percent_fee_deducted_from_returns:
-bybit_testnet_maker_fixed_fees:
-bybit_testnet_taker_fixed_fees:
-hotbit_percent_fee_token:
-hotbit_maker_percent_fee:
-hotbit_taker_percent_fee:
-hotbit_buy_percent_fee_deducted_from_returns:
-hotbit_maker_fixed_fees:
-hotbit_taker_fixed_fees:
-btc_markets_maker_fixed_fees:
-btc_markets_taker_fixed_fees:
-polkadex_percent_fee_token:
-polkadex_maker_percent_fee:
-polkadex_taker_percent_fee:
-polkadex_buy_percent_fee_deducted_from_returns:
-polkadex_maker_fixed_fees:
-polkadex_taker_fixed_fees:
-vertex_percent_fee_token:
-vertex_maker_percent_fee:
-vertex_taker_percent_fee:
-vertex_buy_percent_fee_deducted_from_returns:
-vertex_maker_fixed_fees:
-vertex_taker_fixed_fees:
-vertex_testnet_percent_fee_token:
-vertex_testnet_maker_percent_fee:
-vertex_testnet_taker_percent_fee:
-vertex_testnet_buy_percent_fee_deducted_from_returns:
-vertex_testnet_maker_fixed_fees:
-vertex_testnet_taker_fixed_fees:
-woo_x_percent_fee_token:
-woo_x_maker_percent_fee:
-woo_x_taker_percent_fee:
-woo_x_buy_percent_fee_deducted_from_returns:
-woo_x_maker_fixed_fees:
-woo_x_taker_fixed_fees:
-woo_x_testnet_percent_fee_token:
-woo_x_testnet_maker_percent_fee:
-woo_x_testnet_taker_percent_fee:
-woo_x_testnet_buy_percent_fee_deducted_from_returns:
-woo_x_testnet_maker_fixed_fees:
-woo_x_testnet_taker_fixed_fees:
-injective_v2_percent_fee_token:
-injective_v2_maker_percent_fee:
-injective_v2_taker_percent_fee:
-injective_v2_buy_percent_fee_deducted_from_returns:
-injective_v2_maker_fixed_fees:
-injective_v2_taker_fixed_fees:
-injective_v2_perpetual_percent_fee_token:
-injective_v2_perpetual_maker_percent_fee:
-injective_v2_perpetual_taker_percent_fee:
-injective_v2_perpetual_buy_percent_fee_deducted_from_returns:
-injective_v2_perpetual_maker_fixed_fees:
-injective_v2_perpetual_taker_fixed_fees:
diff --git a/hummingbot_files/templates/master_bot_conf/conf/hummingbot_logs.yml b/hummingbot_files/templates/master_bot_conf/conf/hummingbot_logs.yml
deleted file mode 100755
index 8e65271..0000000
--- a/hummingbot_files/templates/master_bot_conf/conf/hummingbot_logs.yml
+++ /dev/null
@@ -1,83 +0,0 @@
----
-version: 1
-template_version: 12
-
-formatters:
- simple:
- format: "%(asctime)s - %(process)d - %(name)s - %(levelname)s - %(message)s"
-
-handlers:
- console:
- class: hummingbot.logger.cli_handler.CLIHandler
- level: DEBUG
- formatter: simple
- stream: ext://sys.stdout
- console_warning:
- class: hummingbot.logger.cli_handler.CLIHandler
- level: WARNING
- formatter: simple
- stream: ext://sys.stdout
- console_info:
- class: hummingbot.logger.cli_handler.CLIHandler
- level: INFO
- formatter: simple
- stream: ext://sys.stdout
- file_handler:
- class: logging.handlers.TimedRotatingFileHandler
- level: DEBUG
- formatter: simple
- filename: $PROJECT_DIR/logs/logs_$STRATEGY_FILE_PATH.log
- encoding: utf8
- when: "D"
- interval: 1
- backupCount: 7
- "null":
- class: logging.NullHandler
- level: DEBUG
-
-loggers:
- hummingbot.core.utils.eth_gas_station_lookup:
- level: NETWORK
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.logger.log_server_client:
- level: WARNING
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.logger.reporting_proxy_handler:
- level: WARNING
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.strategy:
- level: NETWORK
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.connector:
- level: NETWORK
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.client:
- level: NETWORK
- propagate: false
- handlers: [console, file_handler]
- mqtt: true
- hummingbot.core.event.event_reporter:
- level: EVENT_LOG
- propagate: false
- handlers: [file_handler]
- mqtt: false
- conf:
- level: NETWORK
- handlers: ["null"]
- propagate: false
- mqtt: false
-
-root:
- level: INFO
- handlers: [console, file_handler]
- mqtt: true
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/download_candles.py b/hummingbot_files/templates/master_bot_conf/scripts/download_candles.py
deleted file mode 100644
index 4e56cc2..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/download_candles.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import os
-from typing import Dict
-
-from hummingbot import data_path
-from hummingbot.client.hummingbot_application import HummingbotApplication
-from hummingbot.connector.connector_base import ConnectorBase
-from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class DownloadCandles(ScriptStrategyBase):
- """
- This script provides an example of how to use the Candles Feed to download and store historical data.
- It downloads 3-minute candles for 3 Binance trading pairs ["APE-USDT", "BTC-USDT", "BNB-USDT"] and stores them in
- CSV files in the /data directory. The script stops after it has downloaded 50,000 max_records records for each pair.
- Is important to notice that the component will fail if all the candles are not available since the idea of it is to
- use it in production based on candles needed to compute technical indicators.
- """
- exchange = os.getenv("EXCHANGE", "binance_perpetual")
- trading_pairs = os.getenv("TRADING_PAIRS", "DODO-BUSD,LTC-USDT").split(",")
- intervals = os.getenv("INTERVALS", "1m,3m,5m,1h").split(",")
- days_to_download = int(os.getenv("DAYS_TO_DOWNLOAD", "3"))
- # we can initialize any trading pair since we only need the candles
- markets = {"binance_paper_trade": {"BTC-USDT"}}
-
- @staticmethod
- def get_max_records(days_to_download: int, interval: str) -> int:
- conversion = {"m": 1, "h": 60, "d": 1440}
- unit = interval[-1]
- quantity = int(interval[:-1])
- return int(days_to_download * 24 * 60 / (quantity * conversion[unit]))
-
- def __init__(self, connectors: Dict[str, ConnectorBase]):
- super().__init__(connectors)
- combinations = [(trading_pair, interval) for trading_pair in self.trading_pairs for interval in self.intervals]
-
- self.candles = {f"{combinations[0]}_{combinations[1]}": {} for combinations in combinations}
- # we need to initialize the candles for each trading pair
- for combination in combinations:
-
- candle = CandlesFactory.get_candle(CandlesConfig(connector=self.exchange, trading_pair=combination[0], interval=combination[1], max_records=self.get_max_records(self.days_to_download, combination[1])))
- candle.start()
- # we are storing the candles object and the csv path to save the candles
- self.candles[f"{combination[0]}_{combination[1]}"]["candles"] = candle
- self.candles[f"{combination[0]}_{combination[1]}"][
- "csv_path"] = data_path() + f"/candles_{self.exchange}_{combination[0]}_{combination[1]}.csv"
-
- def on_tick(self):
- for trading_pair, candles_info in self.candles.items():
- if not candles_info["candles"].is_ready:
- self.logger().info(f"Candles not ready yet for {trading_pair}! Missing {candles_info['candles']._candles.maxlen - len(candles_info['candles']._candles)}")
- pass
- else:
- df = candles_info["candles"].candles_df
- df.to_csv(candles_info["csv_path"], index=False)
- if all(candles_info["candles"].is_ready for candles_info in self.candles.values()):
- HummingbotApplication.main_application().stop()
-
- def on_stop(self):
- for candles_info in self.candles.values():
- candles_info["candles"].stop()
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/download_order_book_and_trades.py b/hummingbot_files/templates/master_bot_conf/scripts/download_order_book_and_trades.py
deleted file mode 100644
index d9c754f..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/download_order_book_and_trades.py
+++ /dev/null
@@ -1,99 +0,0 @@
-import json
-import os
-from datetime import datetime
-from typing import Dict
-
-from hummingbot import data_path
-from hummingbot.connector.connector_base import ConnectorBase
-from hummingbot.core.event.event_forwarder import SourceInfoEventForwarder
-from hummingbot.core.event.events import OrderBookEvent, OrderBookTradeEvent
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class DownloadTradesAndOrderBookSnapshots(ScriptStrategyBase):
- exchange = os.getenv("EXCHANGE", "binance_paper_trade")
- trading_pairs = os.getenv("TRADING_PAIRS", "ETH-USDT,BTC-USDT")
- depth = int(os.getenv("DEPTH", 50))
- trading_pairs = [pair for pair in trading_pairs.split(",")]
- last_dump_timestamp = 0
- time_between_csv_dumps = 10
-
- ob_temp_storage = {trading_pair: [] for trading_pair in trading_pairs}
- trades_temp_storage = {trading_pair: [] for trading_pair in trading_pairs}
- current_date = None
- ob_file_paths = {}
- trades_file_paths = {}
- markets = {exchange: set(trading_pairs)}
- subscribed_to_order_book_trade_event: bool = False
-
- def __init__(self, connectors: Dict[str, ConnectorBase]):
- super().__init__(connectors)
- self.create_order_book_and_trade_files()
- self.order_book_trade_event = SourceInfoEventForwarder(self._process_public_trade)
-
- def on_tick(self):
- if not self.subscribed_to_order_book_trade_event:
- self.subscribe_to_order_book_trade_event()
- self.check_and_replace_files()
- for trading_pair in self.trading_pairs:
- order_book_data = self.get_order_book_dict(self.exchange, trading_pair, self.depth)
- self.ob_temp_storage[trading_pair].append(order_book_data)
- if self.last_dump_timestamp < self.current_timestamp:
- self.dump_and_clean_temp_storage()
-
- def get_order_book_dict(self, exchange: str, trading_pair: str, depth: int = 50):
- order_book = self.connectors[exchange].get_order_book(trading_pair)
- snapshot = order_book.snapshot
- return {
- "ts": self.current_timestamp,
- "bids": snapshot[0].loc[:(depth - 1), ["price", "amount"]].values.tolist(),
- "asks": snapshot[1].loc[:(depth - 1), ["price", "amount"]].values.tolist(),
- }
-
- def dump_and_clean_temp_storage(self):
- for trading_pair, order_book_info in self.ob_temp_storage.items():
- file = self.ob_file_paths[trading_pair]
- json_strings = [json.dumps(obj) for obj in order_book_info]
- json_data = '\n'.join(json_strings)
- file.write(json_data)
- self.ob_temp_storage[trading_pair] = []
- for trading_pair, trades_info in self.trades_temp_storage.items():
- file = self.trades_file_paths[trading_pair]
- json_strings = [json.dumps(obj) for obj in trades_info]
- json_data = '\n'.join(json_strings)
- file.write(json_data)
- self.trades_temp_storage[trading_pair] = []
- self.last_dump_timestamp = self.current_timestamp + self.time_between_csv_dumps
-
- def check_and_replace_files(self):
- current_date = datetime.now().strftime("%Y-%m-%d")
- if current_date != self.current_date:
- for file in self.ob_file_paths.values():
- file.close()
- self.create_order_book_and_trade_files()
-
- def create_order_book_and_trade_files(self):
- self.current_date = datetime.now().strftime("%Y-%m-%d")
- self.ob_file_paths = {trading_pair: self.get_file(self.exchange, trading_pair, "order_book_snapshots", self.current_date) for
- trading_pair in self.trading_pairs}
- self.trades_file_paths = {trading_pair: self.get_file(self.exchange, trading_pair, "trades", self.current_date) for
- trading_pair in self.trading_pairs}
-
- @staticmethod
- def get_file(exchange: str, trading_pair: str, source_type: str, current_date: str):
- file_path = data_path() + f"/{exchange}_{trading_pair}_{source_type}_{current_date}.txt"
- return open(file_path, "a")
-
- def _process_public_trade(self, event_tag: int, market: ConnectorBase, event: OrderBookTradeEvent):
- self.trades_temp_storage[event.trading_pair].append({
- "ts": event.timestamp,
- "price": event.price,
- "q_base": event.amount,
- "side": event.type.name.lower(),
- })
-
- def subscribe_to_order_book_trade_event(self):
- for market in self.connectors.values():
- for order_book in market.order_books.values():
- order_book.add_listener(OrderBookEvent.TradeEvent, self.order_book_trade_event)
- self.subscribed_to_order_book_trade_event = True
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/fixed_grid.py b/hummingbot_files/templates/master_bot_conf/scripts/fixed_grid.py
deleted file mode 100644
index 09cfc4a..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/fixed_grid.py
+++ /dev/null
@@ -1,341 +0,0 @@
-import logging
-from decimal import Decimal
-from typing import Dict, List
-
-import numpy as np
-import pandas as pd
-
-from hummingbot.connector.connector_base import ConnectorBase
-from hummingbot.core.data_type.common import OrderType, PriceType, TradeType
-from hummingbot.core.data_type.order_candidate import OrderCandidate
-from hummingbot.core.event.events import BuyOrderCompletedEvent, OrderFilledEvent, SellOrderCompletedEvent
-from hummingbot.core.utils import map_df_to_str
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class FixedGrid(ScriptStrategyBase):
- # Parameters to modify -----------------------------------------
- trading_pair = "ENJ-USDT"
- exchange = "ascend_ex"
- n_levels = 8
- grid_price_ceiling = Decimal(0.33)
- grid_price_floor = Decimal(0.3)
- order_amount = Decimal(18.0)
- # Optional ----------------------
- spread_scale_factor = Decimal(1.0)
- amount_scale_factor = Decimal(1.0)
- rebalance_order_type = "limit"
- rebalance_order_spread = Decimal(0.02)
- rebalance_order_refresh_time = 60.0
- grid_orders_refresh_time = 3600000.0
- price_source = PriceType.MidPrice
- # ----------------------------------------------------------------
-
- markets = {exchange: {trading_pair}}
- create_timestamp = 0
- price_levels = []
- base_inv_levels = []
- quote_inv_levels = []
- order_amount_levels = []
- quote_inv_levels_current_price = []
- current_level = -100
- grid_spread = (grid_price_ceiling - grid_price_floor) / (n_levels - 1)
- inv_correct = True
- rebalance_order_amount = Decimal(0.0)
- rebalance_order_buy = True
-
- def __init__(self, connectors: Dict[str, ConnectorBase]):
- super().__init__(connectors)
-
- self.minimum_spread = (self.grid_price_ceiling - self.grid_price_floor) / (1 + 2 * sum([pow(self.spread_scale_factor, n) for n in range(1, int(self.n_levels / 2))]))
- self.price_levels.append(self.grid_price_floor)
- for i in range(2, int(self.n_levels / 2) + 1):
- price = self.grid_price_floor + self.minimum_spread * sum([pow(self.spread_scale_factor, int(self.n_levels / 2) - n) for n in range(1, i)])
- self.price_levels.append(price)
- for i in range(1, int(self.n_levels / 2) + 1):
- self.order_amount_levels.append(self.order_amount * pow(self.amount_scale_factor, int(self.n_levels / 2) - i))
-
- for i in range(int(self.n_levels / 2) + 1, self.n_levels + 1):
- price = self.price_levels[int(self.n_levels / 2) - 1] + self.minimum_spread * sum([pow(self.spread_scale_factor, n) for n in range(0, i - int(self.n_levels / 2))])
- self.price_levels.append(price)
- self.order_amount_levels.append(self.order_amount * pow(self.amount_scale_factor, i - int(self.n_levels / 2) - 1))
-
- for i in range(1, self.n_levels + 1):
- self.base_inv_levels.append(sum(self.order_amount_levels[i:self.n_levels]))
- self.quote_inv_levels.append(sum([self.price_levels[n] * self.order_amount_levels[n] for n in range(0, i - 1)]))
- for i in range(self.n_levels):
- self.quote_inv_levels_current_price.append(self.quote_inv_levels[i] / self.price_levels[i])
-
- def on_tick(self):
- proposal = None
- if self.create_timestamp <= self.current_timestamp:
- # If grid level not yet set, find it.
- if self.current_level == -100:
- price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source)
- # Find level closest to market
- min_diff = 1e8
- for i in range(self.n_levels):
- if min(min_diff, abs(self.price_levels[i] - price)) < min_diff:
- min_diff = abs(self.price_levels[i] - price)
- self.current_level = i
-
- msg = (f"Current price {price}, Initial level {self.current_level+1}")
- self.log_with_clock(logging.INFO, msg)
- self.notify_hb_app_with_timestamp(msg)
-
- if price > self.grid_price_ceiling:
- msg = ("WARNING: Current price is above grid ceiling")
- self.log_with_clock(logging.WARNING, msg)
- self.notify_hb_app_with_timestamp(msg)
- elif price < self.grid_price_floor:
- msg = ("WARNING: Current price is below grid floor")
- self.log_with_clock(logging.WARNING, msg)
- self.notify_hb_app_with_timestamp(msg)
-
- market, trading_pair, base_asset, quote_asset = self.get_market_trading_pair_tuples()[0]
- base_balance = float(market.get_balance(base_asset))
- quote_balance = float(market.get_balance(quote_asset) / self.price_levels[self.current_level])
-
- if base_balance < self.base_inv_levels[self.current_level]:
- self.inv_correct = False
- msg = (f"WARNING: Insuffient {base_asset} balance for grid bot. Will attempt to rebalance")
- self.log_with_clock(logging.WARNING, msg)
- self.notify_hb_app_with_timestamp(msg)
- if base_balance + quote_balance < self.base_inv_levels[self.current_level] + self.quote_inv_levels_current_price[self.current_level]:
- msg = (f"WARNING: Insuffient {base_asset} and {quote_asset} balance for grid bot. Unable to rebalance."
- f"Please add funds or change grid parameters")
- self.log_with_clock(logging.WARNING, msg)
- self.notify_hb_app_with_timestamp(msg)
- return
- else:
- # Calculate additional base required with 5% tolerance
- base_required = (Decimal(self.base_inv_levels[self.current_level]) - Decimal(base_balance)) * Decimal(1.05)
- self.rebalance_order_buy = True
- self.rebalance_order_amount = Decimal(base_required)
- elif quote_balance < self.quote_inv_levels_current_price[self.current_level]:
- self.inv_correct = False
- msg = (f"WARNING: Insuffient {quote_asset} balance for grid bot. Will attempt to rebalance")
- self.log_with_clock(logging.WARNING, msg)
- self.notify_hb_app_with_timestamp(msg)
- if base_balance + quote_balance < self.base_inv_levels[self.current_level] + self.quote_inv_levels_current_price[self.current_level]:
- msg = (f"WARNING: Insuffient {base_asset} and {quote_asset} balance for grid bot. Unable to rebalance."
- f"Please add funds or change grid parameters")
- self.log_with_clock(logging.WARNING, msg)
- self.notify_hb_app_with_timestamp(msg)
- return
- else:
- # Calculate additional quote required with 5% tolerance
- quote_required = (Decimal(self.quote_inv_levels_current_price[self.current_level]) - Decimal(quote_balance)) * Decimal(1.05)
- self.rebalance_order_buy = False
- self.rebalance_order_amount = Decimal(quote_required)
- else:
- self.inv_correct = True
-
- if self.inv_correct is True:
- # Create proposals for Grid
- proposal = self.create_grid_proposal()
- else:
- # Create rebalance proposal
- proposal = self.create_rebalance_proposal()
-
- self.cancel_active_orders()
- if proposal is not None:
- self.execute_orders_proposal(proposal)
-
- def create_grid_proposal(self) -> List[OrderCandidate]:
- buys = []
- sells = []
-
- # Proposal will be created according to grid price levels
- for i in range(self.current_level):
- price = self.price_levels[i]
- size = self.order_amount_levels[i]
- if size > 0:
- buy_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
- order_side=TradeType.BUY, amount=size, price=price)
- buys.append(buy_order)
-
- for i in range(self.current_level + 1, self.n_levels):
- price = self.price_levels[i]
- size = self.order_amount_levels[i]
- if size > 0:
- sell_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
- order_side=TradeType.SELL, amount=size, price=price)
- sells.append(sell_order)
-
- return buys + sells
-
- def create_rebalance_proposal(self):
- buys = []
- sells = []
-
- # Proposal will be created according to start order spread.
- if self.rebalance_order_buy is True:
- ref_price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source)
- price = ref_price * (Decimal("100") - self.rebalance_order_spread) / Decimal("100")
- size = self.rebalance_order_amount
-
- msg = (f"Placing buy order to rebalance; amount: {size}, price: {price}")
- self.log_with_clock(logging.INFO, msg)
- self.notify_hb_app_with_timestamp(msg)
- if size > 0:
- if self.rebalance_order_type == "limit":
- buy_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
- order_side=TradeType.BUY, amount=size, price=price)
- elif self.rebalance_order_type == "market":
- buy_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.MARKET,
- order_side=TradeType.BUY, amount=size, price=price)
- buys.append(buy_order)
-
- if self.rebalance_order_buy is False:
- ref_price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source)
- price = ref_price * (Decimal("100") + self.rebalance_order_spread) / Decimal("100")
- size = self.rebalance_order_amount
- msg = (f"Placing sell order to rebalance; amount: {size}, price: {price}")
- self.log_with_clock(logging.INFO, msg)
- self.notify_hb_app_with_timestamp(msg)
- if size > 0:
- if self.rebalance_order_type == "limit":
- sell_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
- order_side=TradeType.SELL, amount=size, price=price)
- elif self.rebalance_order_type == "market":
- sell_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.MARKET,
- order_side=TradeType.SELL, amount=size, price=price)
- sells.append(sell_order)
-
- return buys + sells
-
- def did_fill_order(self, event: OrderFilledEvent):
- msg = (f"{event.trade_type.name} {round(event.amount, 2)} {event.trading_pair} {self.exchange} at {round(event.price, 2)}")
- self.log_with_clock(logging.INFO, msg)
- self.notify_hb_app_with_timestamp(msg)
-
- def did_complete_buy_order(self, event: BuyOrderCompletedEvent):
- if self.inv_correct is False:
- self.create_timestamp = self.current_timestamp + float(1.0)
-
- if self.inv_correct is True:
- # Set the new level
- self.current_level -= 1
- # Add sell order above current level
- price = self.price_levels[self.current_level + 1]
- size = self.order_amount_levels[self.current_level + 1]
- proposal = [OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
- order_side=TradeType.SELL, amount=size, price=price)]
- self.execute_orders_proposal(proposal)
-
- def did_complete_sell_order(self, event: SellOrderCompletedEvent):
- if self.inv_correct is False:
- self.create_timestamp = self.current_timestamp + float(1.0)
-
- if self.inv_correct is True:
- # Set the new level
- self.current_level += 1
- # Add buy order above current level
- price = self.price_levels[self.current_level - 1]
- size = self.order_amount_levels[self.current_level - 1]
- proposal = [OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
- order_side=TradeType.BUY, amount=size, price=price)]
- self.execute_orders_proposal(proposal)
-
- def execute_orders_proposal(self, proposal: List[OrderCandidate]) -> None:
- for order in proposal:
- self.place_order(connector_name=self.exchange, order=order)
- if self.inv_correct is False:
- next_cycle = self.current_timestamp + self.rebalance_order_refresh_time
- if self.create_timestamp <= self.current_timestamp:
- self.create_timestamp = next_cycle
- else:
- next_cycle = self.current_timestamp + self.grid_orders_refresh_time
- if self.create_timestamp <= self.current_timestamp:
- self.create_timestamp = next_cycle
-
- def place_order(self, connector_name: str, order: OrderCandidate):
- if order.order_side == TradeType.SELL:
- self.sell(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount,
- order_type=order.order_type, price=order.price)
- elif order.order_side == TradeType.BUY:
- self.buy(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount,
- order_type=order.order_type, price=order.price)
-
- def grid_assets_df(self) -> pd.DataFrame:
- market, trading_pair, base_asset, quote_asset = self.get_market_trading_pair_tuples()[0]
- price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source)
- base_balance = float(market.get_balance(base_asset))
- quote_balance = float(market.get_balance(quote_asset))
- available_base_balance = float(market.get_available_balance(base_asset))
- available_quote_balance = float(market.get_available_balance(quote_asset))
- base_value = base_balance * float(price)
- total_in_quote = base_value + quote_balance
- base_ratio = base_value / total_in_quote if total_in_quote > 0 else 0
- quote_ratio = quote_balance / total_in_quote if total_in_quote > 0 else 0
- data = [
- ["", base_asset, quote_asset],
- ["Total Balance", round(base_balance, 4), round(quote_balance, 4)],
- ["Available Balance", round(available_base_balance, 4), round(available_quote_balance, 4)],
- [f"Current Value ({quote_asset})", round(base_value, 4), round(quote_balance, 4)]
- ]
- data.append(["Current %", f"{base_ratio:.1%}", f"{quote_ratio:.1%}"])
- df = pd.DataFrame(data=data)
- return df
-
- def grid_status_data_frame(self) -> pd.DataFrame:
- grid_data = []
- grid_columns = ["Parameter", "Value"]
-
- market, trading_pair, base_asset, quote_asset = self.get_market_trading_pair_tuples()[0]
- base_balance = float(market.get_balance(base_asset))
- quote_balance = float(market.get_balance(quote_asset) / self.price_levels[self.current_level])
-
- grid_data.append(["Grid spread", round(self.grid_spread, 4)])
- grid_data.append(["Current grid level", self.current_level + 1])
- grid_data.append([f"{base_asset} required", round(self.base_inv_levels[self.current_level], 4)])
- grid_data.append([f"{quote_asset} required in {base_asset}", round(self.quote_inv_levels_current_price[self.current_level], 4)])
- grid_data.append([f"{base_asset} balance", round(base_balance, 4)])
- grid_data.append([f"{quote_asset} balance in {base_asset}", round(quote_balance, 4)])
- grid_data.append(["Correct inventory balance", self.inv_correct])
-
- return pd.DataFrame(data=grid_data, columns=grid_columns).replace(np.nan, '', regex=True)
-
- def format_status(self) -> str:
- """
- Displays the status of the fixed grid strategy
- Returns status of the current strategy on user balances and current active orders.
- """
- if not self.ready_to_trade:
- return "Market connectors are not ready."
-
- lines = []
- warning_lines = []
- warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples()))
-
- balance_df = self.get_balance_df()
- lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")])
-
- grid_df = map_df_to_str(self.grid_status_data_frame())
- lines.extend(["", " Grid:"] + [" " + line for line in grid_df.to_string(index=False).split("\n")])
-
- assets_df = map_df_to_str(self.grid_assets_df())
-
- first_col_length = max(*assets_df[0].apply(len))
- df_lines = assets_df.to_string(index=False, header=False,
- formatters={0: ("{:<" + str(first_col_length) + "}").format}).split("\n")
- lines.extend(["", " Assets:"] + [" " + line for line in df_lines])
-
- try:
- df = self.active_orders_df()
- lines.extend(["", " Orders:"] + [" " + line for line in df.to_string(index=False).split("\n")])
- except ValueError:
- lines.extend(["", " No active maker orders."])
-
- warning_lines.extend(self.balance_warning(self.get_market_trading_pair_tuples()))
- if len(warning_lines) > 0:
- lines.extend(["", "*** WARNINGS ***"] + warning_lines)
- return "\n".join(lines)
-
- def cancel_active_orders(self):
- """
- Cancels active orders
- """
- for order in self.get_active_orders(connector_name=self.exchange):
- self.cancel(self.exchange, order.trading_pair, order.client_order_id)
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/simple_arbitrage_example.py b/hummingbot_files/templates/master_bot_conf/scripts/simple_arbitrage_example.py
deleted file mode 100644
index fb478ab..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/simple_arbitrage_example.py
+++ /dev/null
@@ -1,193 +0,0 @@
-import logging
-from decimal import Decimal
-from typing import Any, Dict
-
-import pandas as pd
-
-from hummingbot.core.data_type.common import OrderType, TradeType
-from hummingbot.core.data_type.order_candidate import OrderCandidate
-from hummingbot.core.event.events import OrderFilledEvent
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class SimpleArbitrage(ScriptStrategyBase):
- """
- BotCamp Cohort: Sept 2022
- Design Template: https://hummingbot-foundation.notion.site/Simple-Arbitrage-51b2af6e54b6493dab12e5d537798c07
- Video: TBD
- Description:
- A simplified version of Hummingbot arbitrage strategy, this bot checks the Volume Weighted Average Price for
- bid and ask in two exchanges and if it finds a profitable opportunity, it will trade the tokens.
- """
- order_amount = Decimal("0.01") # in base asset
- min_profitability = Decimal("0.002") # in percentage
- base = "ETH"
- quote = "USDT"
- trading_pair = f"{base}-{quote}"
- exchange_A = "binance_paper_trade"
- exchange_B = "kucoin_paper_trade"
-
- markets = {exchange_A: {trading_pair},
- exchange_B: {trading_pair}}
-
- def on_tick(self):
- vwap_prices = self.get_vwap_prices_for_amount(self.order_amount)
- proposal = self.check_profitability_and_create_proposal(vwap_prices)
- if len(proposal) > 0:
- proposal_adjusted: Dict[str, OrderCandidate] = self.adjust_proposal_to_budget(proposal)
- self.place_orders(proposal_adjusted)
-
- def get_vwap_prices_for_amount(self, amount: Decimal):
- bid_ex_a = self.connectors[self.exchange_A].get_vwap_for_volume(self.trading_pair, False, amount)
- ask_ex_a = self.connectors[self.exchange_A].get_vwap_for_volume(self.trading_pair, True, amount)
- bid_ex_b = self.connectors[self.exchange_B].get_vwap_for_volume(self.trading_pair, False, amount)
- ask_ex_b = self.connectors[self.exchange_B].get_vwap_for_volume(self.trading_pair, True, amount)
- vwap_prices = {
- self.exchange_A: {
- "bid": bid_ex_a.result_price,
- "ask": ask_ex_a.result_price
- },
- self.exchange_B: {
- "bid": bid_ex_b.result_price,
- "ask": ask_ex_b.result_price
- }
- }
- return vwap_prices
-
- def get_fees_percentages(self, vwap_prices: Dict[str, Any]) -> Dict:
- # We assume that the fee percentage for buying or selling is the same
- a_fee = self.connectors[self.exchange_A].get_fee(
- base_currency=self.base,
- quote_currency=self.quote,
- order_type=OrderType.MARKET,
- order_side=TradeType.BUY,
- amount=self.order_amount,
- price=vwap_prices[self.exchange_A]["ask"],
- is_maker=False
- ).percent
-
- b_fee = self.connectors[self.exchange_B].get_fee(
- base_currency=self.base,
- quote_currency=self.quote,
- order_type=OrderType.MARKET,
- order_side=TradeType.BUY,
- amount=self.order_amount,
- price=vwap_prices[self.exchange_B]["ask"],
- is_maker=False
- ).percent
-
- return {
- self.exchange_A: a_fee,
- self.exchange_B: b_fee
- }
-
- def get_profitability_analysis(self, vwap_prices: Dict[str, Any]) -> Dict:
- fees = self.get_fees_percentages(vwap_prices)
- buy_a_sell_b_quote = vwap_prices[self.exchange_A]["ask"] * (1 - fees[self.exchange_A]) * self.order_amount - \
- vwap_prices[self.exchange_B]["bid"] * (1 + fees[self.exchange_B]) * self.order_amount
- buy_a_sell_b_base = buy_a_sell_b_quote / (
- (vwap_prices[self.exchange_A]["ask"] + vwap_prices[self.exchange_B]["bid"]) / 2)
-
- buy_b_sell_a_quote = vwap_prices[self.exchange_B]["ask"] * (1 - fees[self.exchange_B]) * self.order_amount - \
- vwap_prices[self.exchange_A]["bid"] * (1 + fees[self.exchange_A]) * self.order_amount
-
- buy_b_sell_a_base = buy_b_sell_a_quote / (
- (vwap_prices[self.exchange_B]["ask"] + vwap_prices[self.exchange_A]["bid"]) / 2)
-
- return {
- "buy_a_sell_b":
- {
- "quote_diff": buy_a_sell_b_quote,
- "base_diff": buy_a_sell_b_base,
- "profitability_pct": buy_a_sell_b_base / self.order_amount
- },
- "buy_b_sell_a":
- {
- "quote_diff": buy_b_sell_a_quote,
- "base_diff": buy_b_sell_a_base,
- "profitability_pct": buy_b_sell_a_base / self.order_amount
- },
- }
-
- def check_profitability_and_create_proposal(self, vwap_prices: Dict[str, Any]) -> Dict:
- proposal = {}
- profitability_analysis = self.get_profitability_analysis(vwap_prices)
- if profitability_analysis["buy_a_sell_b"]["profitability_pct"] > self.min_profitability:
- # This means that the ask of the first exchange is lower than the bid of the second one
- proposal[self.exchange_A] = OrderCandidate(trading_pair=self.trading_pair, is_maker=False,
- order_type=OrderType.MARKET,
- order_side=TradeType.BUY, amount=self.order_amount,
- price=vwap_prices[self.exchange_A]["ask"])
- proposal[self.exchange_B] = OrderCandidate(trading_pair=self.trading_pair, is_maker=False,
- order_type=OrderType.MARKET,
- order_side=TradeType.SELL, amount=Decimal(self.order_amount),
- price=vwap_prices[self.exchange_B]["bid"])
- elif profitability_analysis["buy_b_sell_a"]["profitability_pct"] > self.min_profitability:
- # This means that the ask of the second exchange is lower than the bid of the first one
- proposal[self.exchange_B] = OrderCandidate(trading_pair=self.trading_pair, is_maker=False,
- order_type=OrderType.MARKET,
- order_side=TradeType.BUY, amount=self.order_amount,
- price=vwap_prices[self.exchange_B]["ask"])
- proposal[self.exchange_A] = OrderCandidate(trading_pair=self.trading_pair, is_maker=False,
- order_type=OrderType.MARKET,
- order_side=TradeType.SELL, amount=Decimal(self.order_amount),
- price=vwap_prices[self.exchange_A]["bid"])
-
- return proposal
-
- def adjust_proposal_to_budget(self, proposal: Dict[str, OrderCandidate]) -> Dict[str, OrderCandidate]:
- for connector, order in proposal.items():
- proposal[connector] = self.connectors[connector].budget_checker.adjust_candidate(order, all_or_none=True)
- return proposal
-
- def place_orders(self, proposal: Dict[str, OrderCandidate]) -> None:
- for connector, order in proposal.items():
- self.place_order(connector_name=connector, order=order)
-
- def place_order(self, connector_name: str, order: OrderCandidate):
- if order.order_side == TradeType.SELL:
- self.sell(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount,
- order_type=order.order_type, price=order.price)
- elif order.order_side == TradeType.BUY:
- self.buy(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount,
- order_type=order.order_type, price=order.price)
-
- def format_status(self) -> str:
- """
- Returns status of the current strategy on user balances and current active orders. This function is called
- when status command is issued. Override this function to create custom status display output.
- """
- if not self.ready_to_trade:
- return "Market connectors are not ready."
- lines = []
- warning_lines = []
- warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples()))
-
- balance_df = self.get_balance_df()
- lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")])
-
- vwap_prices = self.get_vwap_prices_for_amount(self.order_amount)
- lines.extend(["", " VWAP Prices for amount"] + [" " + line for line in
- pd.DataFrame(vwap_prices).to_string().split("\n")])
- profitability_analysis = self.get_profitability_analysis(vwap_prices)
- lines.extend(["", " Profitability (%)"] + [
- f" Buy A: {self.exchange_A} --> Sell B: {self.exchange_B}"] + [
- f" Quote Diff: {profitability_analysis['buy_a_sell_b']['quote_diff']:.7f}"] + [
- f" Base Diff: {profitability_analysis['buy_a_sell_b']['base_diff']:.7f}"] + [
- f" Percentage: {profitability_analysis['buy_a_sell_b']['profitability_pct'] * 100:.4f} %"] + [
- f" Buy B: {self.exchange_B} --> Sell A: {self.exchange_A}"] + [
- f" Quote Diff: {profitability_analysis['buy_b_sell_a']['quote_diff']:.7f}"] + [
- f" Base Diff: {profitability_analysis['buy_b_sell_a']['base_diff']:.7f}"] + [
- f" Percentage: {profitability_analysis['buy_b_sell_a']['profitability_pct'] * 100:.4f} %"
- ])
-
- warning_lines.extend(self.balance_warning(self.get_market_trading_pair_tuples()))
- if len(warning_lines) > 0:
- lines.extend(["", "*** WARNINGS ***"] + warning_lines)
- return "\n".join(lines)
-
- def did_fill_order(self, event: OrderFilledEvent):
- msg = (
- f"{event.trade_type.name} {round(event.amount, 2)} {event.trading_pair} at {round(event.price, 2)}")
- self.log_with_clock(logging.INFO, msg)
- self.notify_hb_app_with_timestamp(msg)
\ No newline at end of file
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/simple_pmm_example.py b/hummingbot_files/templates/master_bot_conf/scripts/simple_pmm_example.py
deleted file mode 100644
index 8370819..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/simple_pmm_example.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import logging
-from decimal import Decimal
-from typing import List
-
-from hummingbot.core.data_type.common import OrderType, PriceType, TradeType
-from hummingbot.core.data_type.order_candidate import OrderCandidate
-from hummingbot.core.event.events import OrderFilledEvent
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class SimplePMM(ScriptStrategyBase):
- """
- BotCamp Cohort: Sept 2022
- Design Template: https://hummingbot-foundation.notion.site/Simple-PMM-63cc765486dd42228d3da0b32537fc92
- Video: -
- Description:
- The bot will place two orders around the price_source (mid price or last traded price) in a trading_pair on
- exchange, with a distance defined by the ask_spread and bid_spread. Every order_refresh_time in seconds,
- the bot will cancel and replace the orders.
- """
- bid_spread = 0.0001
- ask_spread = 0.0001
- order_refresh_time = 15
- order_amount = 0.01
- create_timestamp = 0
- trading_pair = "ETH-USDT"
- exchange = "binance_paper_trade"
- # Here you can use for example the LastTrade price to use in your strategy
- price_source = PriceType.MidPrice
-
- markets = {exchange: {trading_pair}}
-
- def on_tick(self):
- if self.create_timestamp <= self.current_timestamp:
- self.cancel_all_orders()
- proposal: List[OrderCandidate] = self.create_proposal()
- proposal_adjusted: List[OrderCandidate] = self.adjust_proposal_to_budget(proposal)
- self.place_orders(proposal_adjusted)
- self.create_timestamp = self.order_refresh_time + self.current_timestamp
-
- def create_proposal(self) -> List[OrderCandidate]:
- ref_price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source)
- buy_price = ref_price * Decimal(1 - self.bid_spread)
- sell_price = ref_price * Decimal(1 + self.ask_spread)
-
- buy_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
- order_side=TradeType.BUY, amount=Decimal(self.order_amount), price=buy_price)
-
- sell_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
- order_side=TradeType.SELL, amount=Decimal(self.order_amount), price=sell_price)
-
- return [buy_order, sell_order]
-
- def adjust_proposal_to_budget(self, proposal: List[OrderCandidate]) -> List[OrderCandidate]:
- proposal_adjusted = self.connectors[self.exchange].budget_checker.adjust_candidates(proposal, all_or_none=True)
- return proposal_adjusted
-
- def place_orders(self, proposal: List[OrderCandidate]) -> None:
- for order in proposal:
- self.place_order(connector_name=self.exchange, order=order)
-
- def place_order(self, connector_name: str, order: OrderCandidate):
- if order.order_side == TradeType.SELL:
- self.sell(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount,
- order_type=order.order_type, price=order.price)
- elif order.order_side == TradeType.BUY:
- self.buy(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount,
- order_type=order.order_type, price=order.price)
-
- def cancel_all_orders(self):
- for order in self.get_active_orders(connector_name=self.exchange):
- self.cancel(self.exchange, order.trading_pair, order.client_order_id)
-
- def did_fill_order(self, event: OrderFilledEvent):
- msg = (f"{event.trade_type.name} {round(event.amount, 2)} {event.trading_pair} {self.exchange} at {round(event.price, 2)}")
- self.log_with_clock(logging.INFO, msg)
- self.notify_hb_app_with_timestamp(msg)
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/simple_rsi_example.py b/hummingbot_files/templates/master_bot_conf/scripts/simple_rsi_example.py
deleted file mode 100644
index 7f8fa59..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/simple_rsi_example.py
+++ /dev/null
@@ -1,259 +0,0 @@
-import math
-import os
-from decimal import Decimal
-from typing import Optional
-
-import pandas as pd
-
-from hummingbot.client.hummingbot_application import HummingbotApplication
-from hummingbot.connector.connector_base import ConnectorBase
-from hummingbot.connector.utils import combine_to_hb_trading_pair
-from hummingbot.core.data_type.common import OrderType, TradeType
-from hummingbot.core.data_type.order_candidate import OrderCandidate
-from hummingbot.core.event.event_forwarder import SourceInfoEventForwarder
-from hummingbot.core.event.events import OrderBookEvent, OrderBookTradeEvent, OrderFilledEvent
-from hummingbot.core.rate_oracle.rate_oracle import RateOracle
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class SimpleRSIScript(ScriptStrategyBase):
- """
- The strategy is to buy on overbought signal and sell on oversold.
- """
- connector_name = os.getenv("CONNECTOR_NAME", "binance_paper_trade")
- base = os.getenv("BASE", "BTC")
- quote = os.getenv("QUOTE", "USDT")
- timeframe = os.getenv("TIMEFRAME", "1s")
-
- position_amount_usd = Decimal(os.getenv("POSITION_AMOUNT_USD", "50"))
-
- rsi_length = int(os.getenv("RSI_LENGTH", "14"))
-
- # If true - uses Exponential Moving Average, if false - Simple Moving Average.
- rsi_is_ema = os.getenv("RSI_IS_EMA", 'True').lower() in ('true', '1', 't')
-
- buy_rsi = int(os.getenv("BUY_RSI", "30"))
- sell_rsi = int(os.getenv("SELL_RSI", "70"))
-
- # It depends on a timeframe. Make sure you have enough trades to calculate rsi_length number of candlesticks.
- trade_count_limit = int(os.getenv("TRADE_COUNT_LIMIT", "100000"))
-
- trading_pair = combine_to_hb_trading_pair(base, quote)
- markets = {connector_name: {trading_pair}}
-
- subscribed_to_order_book_trade_event: bool = False
- position: Optional[OrderFilledEvent] = None
-
- _trades: 'list[OrderBookTradeEvent]' = []
- _cumulative_price_change_pct = Decimal(0)
- _filling_position: bool = False
-
- def on_tick(self):
- """
- On every tick calculate OHLCV candlesticks, calculate RSI, react on overbought or oversold signal with creating,
- adjusting and sending an order.
- """
- if not self.subscribed_to_order_book_trade_event:
- # Set pandas resample rule for a timeframe
- self._set_resample_rule(self.timeframe)
- self.subscribe_to_order_book_trade_event()
- elif len(self._trades) > 0:
- df = self.calculate_candlesticks()
- df = self.calculate_rsi(df, self.rsi_length, self.rsi_is_ema)
- should_open_position = self.should_open_position(df)
- should_close_position = self.should_close_position(df)
- if should_open_position or should_close_position:
- order_side = TradeType.BUY if should_open_position else TradeType.SELL
- order_candidate = self.create_order_candidate(order_side)
- # Adjust OrderCandidate
- order_adjusted = self.connectors[self.connector_name].budget_checker.adjust_candidate(order_candidate, all_or_none=False)
- if math.isclose(order_adjusted.amount, Decimal("0"), rel_tol=1E-5):
- self.logger().info(f"Order adjusted: {order_adjusted.amount}, too low to place an order")
- else:
- self.send_order(order_adjusted)
- else:
- self._rsi = df.iloc[-1]['rsi']
- self.logger().info(f"RSI is {self._rsi:.0f}")
-
- def _set_resample_rule(self, timeframe):
- """
- Convert timeframe to pandas resample rule value.
- https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.resample.html
- """
- timeframe_to_rule = {
- "1s": "1S",
- "10s": "10S",
- "30s": "30S",
- "1m": "1T",
- "15m": "15T"
- }
- if timeframe not in timeframe_to_rule.keys():
- self.logger().error(f"{timeframe} timeframe is not mapped to resample rule.")
- HummingbotApplication.main_application().stop()
- self._resample_rule = timeframe_to_rule[timeframe]
-
- def should_open_position(self, df: pd.DataFrame) -> bool:
- """
- If overbought and not in the position.
- """
- rsi: float = df.iloc[-1]['rsi']
- rsi_is_calculated = pd.notna(rsi)
- time_to_buy = rsi_is_calculated and rsi <= self.buy_rsi
- can_buy = self.position is None and not self._filling_position
- return can_buy and time_to_buy
-
- def should_close_position(self, df: pd.DataFrame) -> bool:
- """
- If oversold and in the position.
- """
- rsi: float = df.iloc[-1]['rsi']
- rsi_is_calculated = pd.notna(rsi)
- time_to_sell = rsi_is_calculated and rsi >= self.sell_rsi
- can_sell = self.position is not None and not self._filling_position
- return can_sell and time_to_sell
-
- def create_order_candidate(self, order_side: bool) -> OrderCandidate:
- """
- Create and quantize order candidate.
- """
- connector: ConnectorBase = self.connectors[self.connector_name]
- is_buy = order_side == TradeType.BUY
- price = connector.get_price(self.trading_pair, is_buy)
- if is_buy:
- conversion_rate = RateOracle.get_instance().get_pair_rate(self.trading_pair)
- amount = self.position_amount_usd / conversion_rate
- else:
- amount = self.position.amount
-
- amount = connector.quantize_order_amount(self.trading_pair, amount)
- price = connector.quantize_order_price(self.trading_pair, price)
- return OrderCandidate(
- trading_pair=self.trading_pair,
- is_maker = False,
- order_type = OrderType.LIMIT,
- order_side = order_side,
- amount = amount,
- price = price)
-
- def send_order(self, order: OrderCandidate):
- """
- Send order to the exchange, indicate that position is filling, and send log message with a trade.
- """
- is_buy = order.order_side == TradeType.BUY
- place_order = self.buy if is_buy else self.sell
- place_order(
- connector_name=self.connector_name,
- trading_pair=self.trading_pair,
- amount=order.amount,
- order_type=order.order_type,
- price=order.price
- )
- self._filling_position = True
- if is_buy:
- msg = f"RSI is below {self.buy_rsi:.2f}, buying {order.amount:.5f} {self.base} with limit order at {order.price:.2f} ."
- else:
- msg = (f"RSI is above {self.sell_rsi:.2f}, selling {self.position.amount:.5f} {self.base}"
- f" with limit order at ~ {order.price:.2f}, entry price was {self.position.price:.2f}.")
- self.notify_hb_app_with_timestamp(msg)
- self.logger().info(msg)
-
- def calculate_candlesticks(self) -> pd.DataFrame:
- """
- Convert raw trades to OHLCV dataframe.
- """
- df = pd.DataFrame(self._trades)
- df['timestamp'] = pd.to_datetime(df['timestamp'], unit='s')
- df["timestamp"] = pd.to_datetime(df["timestamp"])
- df.drop(columns=[df.columns[0]], axis=1, inplace=True)
- df = df.set_index('timestamp')
- df = df.resample(self._resample_rule).agg({
- 'price': ['first', 'max', 'min', 'last'],
- 'amount': 'sum',
- })
- df.columns = df.columns.to_flat_index().map(lambda x: x[1])
- df.rename(columns={'first': 'open', 'max': 'high', 'min': 'low', 'last': 'close', 'sum': 'volume'}, inplace=True)
- return df
-
- def did_fill_order(self, event: OrderFilledEvent):
- """
- Indicate that position is filled, save position properties on enter, calculate cumulative price change on exit.
- """
- if event.trade_type == TradeType.BUY:
- self.position = event
- self._filling_position = False
- elif event.trade_type == TradeType.SELL:
- delta_price = (event.price - self.position.price) / self.position.price
- self._cumulative_price_change_pct += delta_price
- self.position = None
- self._filling_position = False
- else:
- self.logger().warn(f"Unsupported order type filled: {event.trade_type}")
-
- @staticmethod
- def calculate_rsi(df: pd.DataFrame, length: int = 14, is_ema: bool = True):
- """
- Calculate relative strength index and add it to the dataframe.
- """
- close_delta = df['close'].diff()
- up = close_delta.clip(lower=0)
- down = close_delta.clip(upper=0).abs()
-
- if is_ema:
- # Exponential Moving Average
- ma_up = up.ewm(com = length - 1, adjust=True, min_periods = length).mean()
- ma_down = down.ewm(com = length - 1, adjust=True, min_periods = length).mean()
- else:
- # Simple Moving Average
- ma_up = up.rolling(window = length, adjust=False).mean()
- ma_down = down.rolling(window = length, adjust=False).mean()
-
- rs = ma_up / ma_down
- df["rsi"] = 100 - (100 / (1 + rs))
- return df
-
- def subscribe_to_order_book_trade_event(self):
- """
- Subscribe to raw trade event.
- """
- self.order_book_trade_event = SourceInfoEventForwarder(self._process_public_trade)
- for market in self.connectors.values():
- for order_book in market.order_books.values():
- order_book.add_listener(OrderBookEvent.TradeEvent, self.order_book_trade_event)
- self.subscribed_to_order_book_trade_event = True
-
- def _process_public_trade(self, event_tag: int, market: ConnectorBase, event: OrderBookTradeEvent):
- """
- Add new trade to list, remove old trade event, if count greater than trade_count_limit.
- """
- if len(self._trades) >= self.trade_count_limit:
- self._trades.pop(0)
- self._trades.append(event)
-
- def format_status(self) -> str:
- """
- Returns status of the current strategy on user balances and current active orders. This function is called
- when status command is issued. Override this function to create custom status display output.
- """
- if not self.ready_to_trade:
- return "Market connectors are not ready."
- lines = []
- warning_lines = []
- warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples()))
-
- balance_df = self.get_balance_df()
- lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")])
-
- try:
- df = self.active_orders_df()
- lines.extend(["", " Orders:"] + [" " + line for line in df.to_string(index=False).split("\n")])
- except ValueError:
- lines.extend(["", " No active maker orders."])
-
- # Strategy specific info
- lines.extend(["", " Current RSI:"] + [" " + f"{self._rsi:.0f}"])
- lines.extend(["", " Simple RSI strategy total price change with all trades:"] + [" " + f"{self._cumulative_price_change_pct:.5f}" + " %"])
-
- warning_lines.extend(self.balance_warning(self.get_market_trading_pair_tuples()))
- if len(warning_lines) > 0:
- lines.extend(["", "*** WARNINGS ***"] + warning_lines)
- return "\n".join(lines)
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/simple_vwap_example.py b/hummingbot_files/templates/master_bot_conf/scripts/simple_vwap_example.py
deleted file mode 100644
index 89dcf08..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/simple_vwap_example.py
+++ /dev/null
@@ -1,184 +0,0 @@
-import logging
-import math
-from decimal import Decimal
-from typing import Dict
-
-from hummingbot.connector.utils import split_hb_trading_pair
-from hummingbot.core.data_type.order_candidate import OrderCandidate
-from hummingbot.core.event.events import OrderFilledEvent, OrderType, TradeType
-from hummingbot.core.rate_oracle.rate_oracle import RateOracle
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class VWAPExample(ScriptStrategyBase):
- """
- BotCamp Cohort: Sept 2022
- Design Template: https://hummingbot-foundation.notion.site/Simple-VWAP-Example-d43a929cc5bd45c6b1a72f63e6635618
- Video: -
- Description:
- This example lets you create one VWAP in a market using a percentage of the sum volume of the order book
- until a spread from the mid price.
- This example demonstrates:
- - How to get the account balance
- - How to get the bids and asks of a market
- - How to code a "utility" strategy
- """
- last_ordered_ts = 0
- vwap: Dict = {"connector_name": "binance_paper_trade", "trading_pair": "ETH-USDT", "is_buy": True,
- "total_volume_usd": 100000, "price_spread": 0.001, "volume_perc": 0.001, "order_delay_time": 10}
- markets = {vwap["connector_name"]: {vwap["trading_pair"]}}
-
- def on_tick(self):
- """
- Every order delay time the strategy will buy or sell the base asset. It will compute the cumulative order book
- volume until the spread and buy a percentage of that.
- The input of the strategy is in USD, but we will use the rate oracle to get a target base that will be static.
- - Use the Rate Oracle to get a conversion rate
- - Create proposal (a list of order candidates)
- - Check the account balance and adjust the proposal accordingly (lower order amount if needed)
- - Lastly, execute the proposal on the exchange
- """
- if self.last_ordered_ts < (self.current_timestamp - self.vwap["order_delay_time"]):
- if self.vwap.get("status") is None:
- self.init_vwap_stats()
- elif self.vwap.get("status") == "ACTIVE":
- vwap_order: OrderCandidate = self.create_order()
- vwap_order_adjusted = self.vwap["connector"].budget_checker.adjust_candidate(vwap_order,
- all_or_none=False)
- if math.isclose(vwap_order_adjusted.amount, Decimal("0"), rel_tol=1E-5):
- self.logger().info(f"Order adjusted: {vwap_order_adjusted.amount}, too low to place an order")
- else:
- self.place_order(
- connector_name=self.vwap["connector_name"],
- trading_pair=self.vwap["trading_pair"],
- is_buy=self.vwap["is_buy"],
- amount=vwap_order_adjusted.amount,
- order_type=vwap_order_adjusted.order_type)
- self.last_ordered_ts = self.current_timestamp
-
- def init_vwap_stats(self):
- # General parameters
- vwap = self.vwap.copy()
- vwap["connector"] = self.connectors[vwap["connector_name"]]
- vwap["delta"] = 0
- vwap["trades"] = []
- vwap["status"] = "ACTIVE"
- vwap["trade_type"] = TradeType.BUY if self.vwap["is_buy"] else TradeType.SELL
- base_asset, quote_asset = split_hb_trading_pair(vwap["trading_pair"])
-
- # USD conversion to quote and base asset
- conversion_base_asset = f"{base_asset}-USD"
- conversion_quote_asset = f"{quote_asset}-USD"
- base_conversion_rate = RateOracle.get_instance().get_pair_rate(conversion_base_asset)
- quote_conversion_rate = RateOracle.get_instance().get_pair_rate(conversion_quote_asset)
- vwap["start_price"] = vwap["connector"].get_price(vwap["trading_pair"], vwap["is_buy"])
- vwap["target_base_volume"] = vwap["total_volume_usd"] / base_conversion_rate
- vwap["ideal_quote_volume"] = vwap["total_volume_usd"] / quote_conversion_rate
-
- # Compute market order scenario
- orderbook_query = vwap["connector"].get_quote_volume_for_base_amount(vwap["trading_pair"], vwap["is_buy"],
- vwap["target_base_volume"])
- vwap["market_order_base_volume"] = orderbook_query.query_volume
- vwap["market_order_quote_volume"] = orderbook_query.result_volume
- vwap["volume_remaining"] = vwap["target_base_volume"]
- vwap["real_quote_volume"] = Decimal(0)
- self.vwap = vwap
-
- def create_order(self) -> OrderCandidate:
- """
- Retrieves the cumulative volume of the order book until the price spread is reached, then takes a percentage
- of that to use as order amount.
- """
- # Compute the new price using the max spread allowed
- mid_price = float(self.vwap["connector"].get_mid_price(self.vwap["trading_pair"]))
- price_multiplier = 1 + self.vwap["price_spread"] if self.vwap["is_buy"] else 1 - self.vwap["price_spread"]
- price_affected_by_spread = mid_price * price_multiplier
-
- # Query the cumulative volume until the price affected by spread
- orderbook_query = self.vwap["connector"].get_volume_for_price(
- trading_pair=self.vwap["trading_pair"],
- is_buy=self.vwap["is_buy"],
- price=price_affected_by_spread)
- volume_for_price = orderbook_query.result_volume
-
- # Check if the volume available is higher than the remaining
- amount = min(volume_for_price * Decimal(self.vwap["volume_perc"]), Decimal(self.vwap["volume_remaining"]))
-
- # Quantize the order amount and price
- amount = self.vwap["connector"].quantize_order_amount(self.vwap["trading_pair"], amount)
- price = self.vwap["connector"].quantize_order_price(self.vwap["trading_pair"],
- Decimal(price_affected_by_spread))
- # Create the Order Candidate
- vwap_order = OrderCandidate(
- trading_pair=self.vwap["trading_pair"],
- is_maker=False,
- order_type=OrderType.MARKET,
- order_side=self.vwap["trade_type"],
- amount=amount,
- price=price)
- return vwap_order
-
- def place_order(self,
- connector_name: str,
- trading_pair: str,
- is_buy: bool,
- amount: Decimal,
- order_type: OrderType,
- price=Decimal("NaN"),
- ):
- if is_buy:
- self.buy(connector_name, trading_pair, amount, order_type, price)
- else:
- self.sell(connector_name, trading_pair, amount, order_type, price)
-
- def did_fill_order(self, event: OrderFilledEvent):
- """
- Listens to fill order event to log it and notify the Hummingbot application.
- If you set up Telegram bot, you will get notification there as well.
- """
- if event.trading_pair == self.vwap["trading_pair"] and event.trade_type == self.vwap["trade_type"]:
- self.vwap["volume_remaining"] -= event.amount
- self.vwap["delta"] = (self.vwap["target_base_volume"] - self.vwap["volume_remaining"]) / self.vwap[
- "target_base_volume"]
- self.vwap["real_quote_volume"] += event.price * event.amount
- self.vwap["trades"].append(event)
- if math.isclose(self.vwap["delta"], 1, rel_tol=1e-5):
- self.vwap["status"] = "COMPLETE"
- msg = (f"({event.trading_pair}) {event.trade_type.name} order (price: {round(event.price, 2)}) of "
- f"{round(event.amount, 2)} "
- f"{split_hb_trading_pair(event.trading_pair)[0]} is filled.")
-
- self.log_with_clock(logging.INFO, msg)
- self.notify_hb_app_with_timestamp(msg)
-
- def format_status(self) -> str:
- """
- Returns status of the current strategy on user balances and current active orders. This function is called
- when status command is issued. Override this function to create custom status display output.
- """
- if not self.ready_to_trade:
- return "Market connectors are not ready."
- lines = []
- warning_lines = []
- warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples()))
-
- balance_df = self.get_balance_df()
- lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")])
-
- try:
- df = self.active_orders_df()
- lines.extend(["", " Orders:"] + [" " + line for line in df.to_string(index=False).split("\n")])
- except ValueError:
- lines.extend(["", " No active maker orders."])
- lines.extend(["", "VWAP Info:"] + [" " + key + ": " + value
- for key, value in self.vwap.items()
- if type(value) == str])
-
- lines.extend(["", "VWAP Stats:"] + [" " + key + ": " + str(round(value, 4))
- for key, value in self.vwap.items()
- if type(value) in [int, float, Decimal]])
-
- warning_lines.extend(self.balance_warning(self.get_market_trading_pair_tuples()))
- if len(warning_lines) > 0:
- lines.extend(["", "*** WARNINGS ***"] + warning_lines)
- return "\n".join(lines)
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/simple_xemm_example.py b/hummingbot_files/templates/master_bot_conf/scripts/simple_xemm_example.py
deleted file mode 100644
index 3c8244b..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/simple_xemm_example.py
+++ /dev/null
@@ -1,204 +0,0 @@
-from decimal import Decimal
-
-import pandas as pd
-
-from hummingbot.core.data_type.common import OrderType, TradeType
-from hummingbot.core.data_type.order_candidate import OrderCandidate
-from hummingbot.core.event.events import OrderFilledEvent
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class SimpleXEMM(ScriptStrategyBase):
- """
- BotCamp Cohort: Sept 2022
- Design Template: https://hummingbot-foundation.notion.site/Simple-XEMM-Example-f08cf7546ea94a44b389672fd21bb9ad
- Video: https://www.loom.com/share/ca08fe7bc3d14ba68ae704305ac78a3a
- Description:
- A simplified version of Hummingbot cross-exchange market making strategy, this bot makes a market on
- the maker pair and hedges any filled trades in the taker pair. If the spread (difference between maker order price
- and taker hedge price) dips below min_spread, the bot refreshes the order
- """
-
- maker_exchange = "kucoin_paper_trade"
- maker_pair = "ETH-USDT"
- taker_exchange = "gate_io_paper_trade"
- taker_pair = "ETH-USDT"
-
- order_amount = 0.1 # amount for each order
- spread_bps = 10 # bot places maker orders at this spread to taker price
- min_spread_bps = 0 # bot refreshes order if spread is lower than min-spread
- slippage_buffer_spread_bps = 100 # buffer applied to limit taker hedging trades on taker exchange
- max_order_age = 120 # bot refreshes orders after this age
-
- markets = {maker_exchange: {maker_pair}, taker_exchange: {taker_pair}}
-
- buy_order_placed = False
- sell_order_placed = False
-
- def on_tick(self):
- taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
- taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount)
-
- if not self.buy_order_placed:
- maker_buy_price = taker_sell_result.result_price * Decimal(1 - self.spread_bps / 10000)
- buy_order_amount = min(self.order_amount, self.buy_hedging_budget())
- buy_order = OrderCandidate(trading_pair=self.maker_pair, is_maker=True, order_type=OrderType.LIMIT, order_side=TradeType.BUY, amount=Decimal(buy_order_amount), price=maker_buy_price)
- buy_order_adjusted = self.connectors[self.maker_exchange].budget_checker.adjust_candidate(buy_order, all_or_none=False)
- self.buy(self.maker_exchange, self.maker_pair, buy_order_adjusted.amount, buy_order_adjusted.order_type, buy_order_adjusted.price)
- self.buy_order_placed = True
-
- if not self.sell_order_placed:
- maker_sell_price = taker_buy_result.result_price * Decimal(1 + self.spread_bps / 10000)
- sell_order_amount = min(self.order_amount, self.sell_hedging_budget())
- sell_order = OrderCandidate(trading_pair=self.maker_pair, is_maker=True, order_type=OrderType.LIMIT, order_side=TradeType.SELL, amount=Decimal(sell_order_amount), price=maker_sell_price)
- sell_order_adjusted = self.connectors[self.maker_exchange].budget_checker.adjust_candidate(sell_order, all_or_none=False)
- self.sell(self.maker_exchange, self.maker_pair, sell_order_adjusted.amount, sell_order_adjusted.order_type, sell_order_adjusted.price)
- self.sell_order_placed = True
-
- for order in self.get_active_orders(connector_name=self.maker_exchange):
- cancel_timestamp = order.creation_timestamp / 1000000 + self.max_order_age
- if order.is_buy:
- buy_cancel_threshold = taker_sell_result.result_price * Decimal(1 - self.min_spread_bps / 10000)
- if order.price > buy_cancel_threshold or cancel_timestamp < self.current_timestamp:
- self.logger().info(f"Cancelling buy order: {order.client_order_id}")
- self.cancel(self.maker_exchange, order.trading_pair, order.client_order_id)
- self.buy_order_placed = False
- else:
- sell_cancel_threshold = taker_buy_result.result_price * Decimal(1 + self.min_spread_bps / 10000)
- if order.price < sell_cancel_threshold or cancel_timestamp < self.current_timestamp:
- self.logger().info(f"Cancelling sell order: {order.client_order_id}")
- self.cancel(self.maker_exchange, order.trading_pair, order.client_order_id)
- self.sell_order_placed = False
- return
-
- def buy_hedging_budget(self) -> Decimal:
- balance = self.connectors[self.taker_exchange].get_available_balance("ETH")
- return balance
-
- def sell_hedging_budget(self) -> Decimal:
- balance = self.connectors[self.taker_exchange].get_available_balance("USDT")
- taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
- return balance / taker_buy_result.result_price
-
- def is_active_maker_order(self, event: OrderFilledEvent):
- """
- Helper function that checks if order is an active order on the maker exchange
- """
- for order in self.get_active_orders(connector_name=self.maker_exchange):
- if order.client_order_id == event.order_id:
- return True
- return False
-
- def did_fill_order(self, event: OrderFilledEvent):
-
- mid_price = self.connectors[self.maker_exchange].get_mid_price(self.maker_pair)
- if event.trade_type == TradeType.BUY and self.is_active_maker_order(event):
- taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount)
- sell_price_with_slippage = taker_sell_result.result_price * Decimal(1 - self.slippage_buffer_spread_bps / 10000)
- self.logger().info(f"Filled maker buy order with price: {event.price}")
- sell_spread_bps = (taker_sell_result.result_price - event.price) / mid_price * 10000
- self.logger().info(f"Sending taker sell order at price: {taker_sell_result.result_price} spread: {int(sell_spread_bps)} bps")
- sell_order = OrderCandidate(trading_pair=self.taker_pair, is_maker=False, order_type=OrderType.LIMIT, order_side=TradeType.SELL, amount=Decimal(event.amount), price=sell_price_with_slippage)
- sell_order_adjusted = self.connectors[self.taker_exchange].budget_checker.adjust_candidate(sell_order, all_or_none=False)
- self.sell(self.taker_exchange, self.taker_pair, sell_order_adjusted.amount, sell_order_adjusted.order_type, sell_order_adjusted.price)
- self.buy_order_placed = False
- else:
- if event.trade_type == TradeType.SELL and self.is_active_maker_order(event):
- taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
- buy_price_with_slippage = taker_buy_result.result_price * Decimal(1 + self.slippage_buffer_spread_bps / 10000)
- buy_spread_bps = (event.price - taker_buy_result.result_price) / mid_price * 10000
- self.logger().info(f"Filled maker sell order at price: {event.price}")
- self.logger().info(f"Sending taker buy order: {taker_buy_result.result_price} spread: {int(buy_spread_bps)}")
- buy_order = OrderCandidate(trading_pair=self.taker_pair, is_maker=False, order_type=OrderType.LIMIT, order_side=TradeType.BUY, amount=Decimal(event.amount), price=buy_price_with_slippage)
- buy_order_adjusted = self.connectors[self.taker_exchange].budget_checker.adjust_candidate(buy_order, all_or_none=False)
- self.buy(self.taker_exchange, self.taker_pair, buy_order_adjusted.amount, buy_order_adjusted.order_type, buy_order_adjusted.price)
- self.sell_order_placed = False
-
- def exchanges_df(self) -> pd.DataFrame:
- """
- Return a custom data frame of prices on maker vs taker exchanges for display purposes
- """
- mid_price = self.connectors[self.maker_exchange].get_mid_price(self.maker_pair)
- maker_buy_result = self.connectors[self.maker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
- maker_sell_result = self.connectors[self.maker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount)
- taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
- taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount)
- maker_buy_spread_bps = (maker_buy_result.result_price - taker_buy_result.result_price) / mid_price * 10000
- maker_sell_spread_bps = (taker_sell_result.result_price - maker_sell_result.result_price) / mid_price * 10000
- columns = ["Exchange", "Market", "Mid Price", "Buy Price", "Sell Price", "Buy Spread", "Sell Spread"]
- data = []
- data.append([
- self.maker_exchange,
- self.maker_pair,
- float(self.connectors[self.maker_exchange].get_mid_price(self.maker_pair)),
- float(maker_buy_result.result_price),
- float(maker_sell_result.result_price),
- int(maker_buy_spread_bps),
- int(maker_sell_spread_bps)
- ])
- data.append([
- self.taker_exchange,
- self.taker_pair,
- float(self.connectors[self.taker_exchange].get_mid_price(self.maker_pair)),
- float(taker_buy_result.result_price),
- float(taker_sell_result.result_price),
- int(-maker_buy_spread_bps),
- int(-maker_sell_spread_bps)
- ])
- df = pd.DataFrame(data=data, columns=columns)
- return df
-
- def active_orders_df(self) -> pd.DataFrame:
- """
- Returns a custom data frame of all active maker orders for display purposes
- """
- columns = ["Exchange", "Market", "Side", "Price", "Amount", "Spread Mid", "Spread Cancel", "Age"]
- data = []
- mid_price = self.connectors[self.maker_exchange].get_mid_price(self.maker_pair)
- taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
- taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount)
- buy_cancel_threshold = taker_sell_result.result_price * Decimal(1 - self.min_spread_bps / 10000)
- sell_cancel_threshold = taker_buy_result.result_price * Decimal(1 + self.min_spread_bps / 10000)
- for connector_name, connector in self.connectors.items():
- for order in self.get_active_orders(connector_name):
- age_txt = "n/a" if order.age() <= 0. else pd.Timestamp(order.age(), unit='s').strftime('%H:%M:%S')
- spread_mid_bps = (mid_price - order.price) / mid_price * 10000 if order.is_buy else (order.price - mid_price) / mid_price * 10000
- spread_cancel_bps = (buy_cancel_threshold - order.price) / buy_cancel_threshold * 10000 if order.is_buy else (order.price - sell_cancel_threshold) / sell_cancel_threshold * 10000
- data.append([
- self.maker_exchange,
- order.trading_pair,
- "buy" if order.is_buy else "sell",
- float(order.price),
- float(order.quantity),
- int(spread_mid_bps),
- int(spread_cancel_bps),
- age_txt
- ])
- if not data:
- raise ValueError
- df = pd.DataFrame(data=data, columns=columns)
- df.sort_values(by=["Market", "Side"], inplace=True)
- return df
-
- def format_status(self) -> str:
- """
- Returns status of the current strategy on user balances and current active orders. This function is called
- when status command is issued. Override this function to create custom status display output.
- """
- if not self.ready_to_trade:
- return "Market connectors are not ready."
- lines = []
-
- balance_df = self.get_balance_df()
- lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")])
-
- exchanges_df = self.exchanges_df()
- lines.extend(["", " Exchanges:"] + [" " + line for line in exchanges_df.to_string(index=False).split("\n")])
-
- try:
- orders_df = self.active_orders_df()
- lines.extend(["", " Active Orders:"] + [" " + line for line in orders_df.to_string(index=False).split("\n")])
- except ValueError:
- lines.extend(["", " No active maker orders."])
-
- return "\n".join(lines)
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/strategy_v2_launcher.py b/hummingbot_files/templates/master_bot_conf/scripts/strategy_v2_launcher.py
deleted file mode 100644
index 392972b..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/strategy_v2_launcher.py
+++ /dev/null
@@ -1,110 +0,0 @@
-import inspect
-import os
-import importlib.util
-
-from hummingbot.core.data_type.common import OrderType, PositionMode, TradeType, PositionSide, PositionAction
-from hummingbot.smart_components.strategy_frameworks.data_types import (
- ExecutorHandlerStatus,
-)
-from hummingbot.smart_components.strategy_frameworks.directional_trading import DirectionalTradingControllerBase, \
- DirectionalTradingControllerConfigBase, DirectionalTradingExecutorHandler
-from hummingbot.smart_components.utils.config_encoder_decoder import ConfigEncoderDecoder
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-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
- if issubclass(cls, DirectionalTradingControllerConfigBase) and cls is not DirectionalTradingControllerConfigBase:
- controllers[module_name]["config"] = cls
- return controllers
-
-
-def initialize_controller_from_config(encoder_decoder: ConfigEncoderDecoder,
- all_controllers_info: dict,
- controller_config_file: str):
- config = encoder_decoder.yaml_load(f"conf/controllers_config/{controller_config_file}")
- controller_info = all_controllers_info[config["strategy_name"]]
- config_instance = controller_info["config"](**config)
- controller_class = controller_info["class"](config_instance)
- return controller_class
-
-
-class StrategyV2Launcher(ScriptStrategyBase):
- controller_configs = os.getenv("controller_configs", "bollinger_8044.yml,bollinger_8546.yml,bollinger_8883.yml")
- controllers = {}
- markets = {}
- executor_handlers = {}
- encoder_decoder = ConfigEncoderDecoder(TradeType, PositionMode, OrderType)
- controllers_info = load_controllers("hummingbot/smart_components/controllers")
-
- for controller_config in controller_configs.split(","):
- controller = initialize_controller_from_config(encoder_decoder, controllers_info, controller_config)
- markets = controller.update_strategy_markets_dict(markets)
- controllers[controller_config] = controller
-
- def __init__(self, connectors):
- super().__init__(connectors)
- for controller_config, controller in self.controllers.items():
- self.executor_handlers[controller_config] = DirectionalTradingExecutorHandler(strategy=self, controller=controller)
-
- def on_stop(self):
- for connector in self.connectors.keys():
- if self.is_perpetual(connector):
- self.close_open_positions(connector)
- for executor_handler in self.executor_handlers.values():
- executor_handler.stop()
-
- @staticmethod
- def is_perpetual(exchange):
- """
- Checks if the exchange is a perpetual market.
- """
- return "perpetual" in exchange
-
- def close_open_positions(self, exchange):
- connector = self.connectors[exchange]
- for trading_pair, position in connector.account_positions.items():
- if position.position_side == PositionSide.LONG:
- self.sell(connector_name=exchange,
- trading_pair=position.trading_pair,
- amount=abs(position.amount),
- order_type=OrderType.MARKET,
- price=connector.get_mid_price(position.trading_pair),
- position_action=PositionAction.CLOSE)
- elif position.position_side == PositionSide.SHORT:
- self.buy(connector_name=exchange,
- trading_pair=position.trading_pair,
- amount=abs(position.amount),
- order_type=OrderType.MARKET,
- price=connector.get_mid_price(position.trading_pair),
- position_action=PositionAction.CLOSE)
-
- def on_tick(self):
- """
- This shows you how you can start meta controllers. You can run more than one at the same time and based on the
- market conditions, you can orchestrate from this script when to stop or start them.
- """
- for executor_handler in self.executor_handlers.values():
- if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED:
- executor_handler.start()
-
- def format_status(self) -> str:
- if not self.ready_to_trade:
- return "Market connectors are not ready."
- lines = []
- for controller_config, executor_handler in self.executor_handlers.items():
- lines.extend(["\n------------------------------------------------------------------------------------------"])
- lines.extend([f"Strategy: {executor_handler.controller.config.strategy_name} | Config: {controller_config}",
- executor_handler.to_format_status()])
- return "\n".join(lines)
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/v2_directional-trading_macd_bb_v1.py b/hummingbot_files/templates/master_bot_conf/scripts/v2_directional-trading_macd_bb_v1.py
deleted file mode 100644
index 2e72ccd..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/v2_directional-trading_macd_bb_v1.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from decimal import Decimal
-from typing import Dict
-
-from hummingbot.connector.connector_base import ConnectorBase, TradeType
-from hummingbot.core.data_type.common import OrderType
-from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
-from hummingbot.smart_components.controllers.macd_bb_v1 import MACDBBV1, MACDBBV1Config
-from hummingbot.smart_components.strategy_frameworks.data_types import (
- ExecutorHandlerStatus,
- OrderLevel,
- TripleBarrierConf,
-)
-from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_executor_handler import (
- DirectionalTradingExecutorHandler,
-)
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class MarketMakingDmanComposed(ScriptStrategyBase):
- trading_pairs = ["HBAR-USDT", "CYBER-USDT", "ETH-USDT", "LPT-USDT", "UNFI-USDT"]
- leverage_by_trading_pair = {
- "HBAR-USDT": 25,
- "CYBER-USDT": 20,
- "ETH-USDT": 100,
- "LPT-USDT": 10,
- "UNFI-USDT": 20,
- }
- triple_barrier_conf = TripleBarrierConf(
- stop_loss=Decimal("0.01"), take_profit=Decimal("0.03"),
- time_limit=60 * 60 * 6,
- trailing_stop_activation_price_delta=Decimal("0.008"),
- trailing_stop_trailing_delta=Decimal("0.004"),
- open_order_type=OrderType.MARKET
- )
-
- order_levels = [
- OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal("15"),
- spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
- OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal("15"),
- spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
- ]
- controllers = {}
- markets = {}
- executor_handlers = {}
-
- for trading_pair in trading_pairs:
- config = MACDBBV1Config(
- exchange="binance_perpetual",
- trading_pair=trading_pair,
- order_levels=order_levels,
- candles_config=[
- CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=100),
- ],
- leverage=leverage_by_trading_pair[trading_pair],
- macd_fast=21, macd_slow=42, macd_signal=9,
- bb_length=100, bb_std=2.0, bb_long_threshold=0.3, bb_short_threshold=0.7,
- )
- controller = MACDBBV1(config=config)
- markets = controller.update_strategy_markets_dict(markets)
- controllers[trading_pair] = controller
-
- def __init__(self, connectors: Dict[str, ConnectorBase]):
- super().__init__(connectors)
- for trading_pair, controller in self.controllers.items():
- self.executor_handlers[trading_pair] = DirectionalTradingExecutorHandler(strategy=self, controller=controller)
-
- def on_stop(self):
- for executor_handler in self.executor_handlers.values():
- executor_handler.stop()
-
- def on_tick(self):
- """
- This shows you how you can start meta controllers. You can run more than one at the same time and based on the
- market conditions, you can orchestrate from this script when to stop or start them.
- """
- for executor_handler in self.executor_handlers.values():
- if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED:
- executor_handler.start()
-
- def format_status(self) -> str:
- if not self.ready_to_trade:
- return "Market connectors are not ready."
- lines = []
- for trading_pair, executor_handler in self.executor_handlers.items():
- if executor_handler.controller.all_candles_ready:
- lines.extend(
- [f"Strategy: {executor_handler.controller.config.strategy_name} | Trading Pair: {trading_pair}",
- executor_handler.to_format_status()])
- return "\n".join(lines)
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_composed.py b/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_composed.py
deleted file mode 100644
index c539897..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_composed.py
+++ /dev/null
@@ -1,145 +0,0 @@
-from decimal import Decimal
-from typing import Dict
-
-from hummingbot.connector.connector_base import ConnectorBase, TradeType
-from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide
-from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
-from hummingbot.smart_components.controllers.dman_v1 import DManV1, DManV1Config
-from hummingbot.smart_components.controllers.dman_v2 import DManV2, DManV2Config
-from hummingbot.smart_components.strategy_frameworks.data_types import (
- ExecutorHandlerStatus,
- OrderLevel,
- TripleBarrierConf,
-)
-from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import (
- MarketMakingExecutorHandler,
-)
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class MarketMakingDmanComposed(ScriptStrategyBase):
- trading_pair = "HBAR-USDT"
- triple_barrier_conf_top = TripleBarrierConf(
- stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"),
- time_limit=60 * 60 * 1,
- trailing_stop_activation_price_delta=Decimal("0.002"),
- trailing_stop_trailing_delta=Decimal("0.0005")
- )
- triple_barrier_conf_bottom = TripleBarrierConf(
- stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"),
- time_limit=60 * 60 * 3,
- trailing_stop_activation_price_delta=Decimal("0.005"),
- trailing_stop_trailing_delta=Decimal("0.001")
- )
-
- config_v1 = DManV1Config(
- exchange="binance_perpetual",
- trading_pair=trading_pair,
- order_levels=[
- OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal("15"),
- spread_factor=Decimal(1.0), order_refresh_time=60 * 30,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top),
- OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal("50"),
- spread_factor=Decimal(5.0), order_refresh_time=60 * 30,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
- OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal("50"),
- spread_factor=Decimal(8.0), order_refresh_time=60 * 15,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
- OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal("15"),
- spread_factor=Decimal(1.0), order_refresh_time=60 * 30,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top),
- OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal("50"),
- spread_factor=Decimal(5.0), order_refresh_time=60 * 30,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
- OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal("50"),
- spread_factor=Decimal(8.0), order_refresh_time=60 * 15,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
- ],
- candles_config=[
- CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=1000),
- ],
- leverage=25,
- natr_length=21
- )
- config_v2 = DManV2Config(
- exchange="binance_perpetual",
- trading_pair=trading_pair,
- order_levels=[
- OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(15),
- spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top),
- OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal(30),
- spread_factor=Decimal(2.0), order_refresh_time=60 * 5,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
- OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal(50),
- spread_factor=Decimal(3.0), order_refresh_time=60 * 15,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
- OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(15),
- spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top),
- OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal(30),
- spread_factor=Decimal(2.0), order_refresh_time=60 * 5,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
- OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal(50),
- spread_factor=Decimal(3.0), order_refresh_time=60 * 15,
- cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
- ],
- candles_config=[
- CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=1000),
- ],
- leverage=25,
- natr_length=21, macd_fast=12, macd_slow=26, macd_signal=9
- )
- dman_v1 = DManV1(config=config_v1)
- dman_v2 = DManV2(config=config_v2)
-
- empty_markets = {}
- markets = dman_v1.update_strategy_markets_dict(empty_markets)
- markets = dman_v2.update_strategy_markets_dict(markets)
-
- def __init__(self, connectors: Dict[str, ConnectorBase]):
- super().__init__(connectors)
- self.dman_v1_executor = MarketMakingExecutorHandler(strategy=self, controller=self.dman_v1)
- self.dman_v2_executor = MarketMakingExecutorHandler(strategy=self, controller=self.dman_v2)
-
- def on_stop(self):
- self.close_open_positions()
-
- def on_tick(self):
- """
- This shows you how you can start meta controllers. You can run more than one at the same time and based on the
- market conditions, you can orchestrate from this script when to stop or start them.
- """
- if self.dman_v1_executor.status == ExecutorHandlerStatus.NOT_STARTED:
- self.dman_v1_executor.start()
- if self.dman_v2_executor.status == ExecutorHandlerStatus.NOT_STARTED:
- self.dman_v2_executor.start()
-
- def format_status(self) -> str:
- if not self.ready_to_trade:
- return "Market connectors are not ready."
- lines = []
- lines.extend(["DMAN V1", self.dman_v1_executor.to_format_status()])
- lines.extend(["\n-----------------------------------------\n"])
- lines.extend(["DMAN V2", self.dman_v2_executor.to_format_status()])
- return "\n".join(lines)
-
- def close_open_positions(self):
- # we are going to close all the open positions when the bot stops
- for connector_name, connector in self.connectors.items():
- for trading_pair, position in connector.account_positions.items():
- if trading_pair in self.markets[connector_name]:
- if position.position_side == PositionSide.LONG:
- self.sell(connector_name=connector_name,
- trading_pair=position.trading_pair,
- amount=abs(position.amount),
- order_type=OrderType.MARKET,
- price=connector.get_mid_price(position.trading_pair),
- position_action=PositionAction.CLOSE)
- elif position.position_side == PositionSide.SHORT:
- self.buy(connector_name=connector_name,
- trading_pair=position.trading_pair,
- amount=abs(position.amount),
- order_type=OrderType.MARKET,
- price=connector.get_mid_price(position.trading_pair),
- position_action=PositionAction.CLOSE)
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_v1_multiple_pairs.py b/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_v1_multiple_pairs.py
deleted file mode 100644
index 24189cf..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_v1_multiple_pairs.py
+++ /dev/null
@@ -1,133 +0,0 @@
-from decimal import Decimal
-from typing import Dict
-
-from hummingbot.connector.connector_base import ConnectorBase
-from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide
-from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
-from hummingbot.smart_components.controllers.dman_v1 import DManV1, DManV1Config
-from hummingbot.smart_components.strategy_frameworks.data_types import ExecutorHandlerStatus, TripleBarrierConf
-from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import (
- MarketMakingExecutorHandler,
-)
-from hummingbot.smart_components.utils.distributions import Distributions
-from hummingbot.smart_components.utils.order_level_builder import OrderLevelBuilder
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class DManV1MultiplePairs(ScriptStrategyBase):
- # Account configuration
- exchange = "binance_perpetual"
- trading_pairs = ["ETH-USDT"]
- leverage = 20
-
- # Candles configuration
- candles_exchange = "binance_perpetual"
- candles_interval = "3m"
- candles_max_records = 300
-
- # Orders configuration
- order_amount = Decimal("25")
- n_levels = 5
- start_spread = 0.0006
- step_between_orders = 0.009
- order_refresh_time = 60 * 15 # 15 minutes
- cooldown_time = 5
-
- # Triple barrier configuration
- stop_loss = Decimal("0.2")
- take_profit = Decimal("0.06")
- time_limit = 60 * 60 * 12
- trailing_stop_activation_price_delta = Decimal(str(step_between_orders / 2))
- trailing_stop_trailing_delta = Decimal(str(step_between_orders / 3))
-
- # Advanced configurations
- natr_length = 100
-
- # Applying the configuration
- order_level_builder = OrderLevelBuilder(n_levels=n_levels)
- order_levels = order_level_builder.build_order_levels(
- amounts=order_amount,
- spreads=Distributions.arithmetic(n_levels=n_levels, start=start_spread, step=step_between_orders),
- triple_barrier_confs=TripleBarrierConf(
- stop_loss=stop_loss, take_profit=take_profit, time_limit=time_limit,
- trailing_stop_activation_price_delta=trailing_stop_activation_price_delta,
- trailing_stop_trailing_delta=trailing_stop_trailing_delta),
- order_refresh_time=order_refresh_time,
- cooldown_time=cooldown_time,
- )
- controllers = {}
- markets = {}
- executor_handlers = {}
-
- for trading_pair in trading_pairs:
- config = DManV1Config(
- exchange=exchange,
- trading_pair=trading_pair,
- order_levels=order_levels,
- candles_config=[
- CandlesConfig(connector=candles_exchange, trading_pair=trading_pair,
- interval=candles_interval, max_records=candles_max_records),
- ],
- leverage=leverage,
- natr_length=natr_length,
- )
- controller = DManV1(config=config)
- markets = controller.update_strategy_markets_dict(markets)
- controllers[trading_pair] = controller
-
- def __init__(self, connectors: Dict[str, ConnectorBase]):
- super().__init__(connectors)
- for trading_pair, controller in self.controllers.items():
- self.executor_handlers[trading_pair] = MarketMakingExecutorHandler(strategy=self, controller=controller)
-
- @property
- def is_perpetual(self):
- """
- Checks if the exchange is a perpetual market.
- """
- return "perpetual" in self.exchange
-
- def on_stop(self):
- if self.is_perpetual:
- self.close_open_positions()
- for executor_handler in self.executor_handlers.values():
- executor_handler.stop()
-
- def close_open_positions(self):
- # we are going to close all the open positions when the bot stops
- for connector_name, connector in self.connectors.items():
- for trading_pair, position in connector.account_positions.items():
- if trading_pair in self.markets[connector_name]:
- if position.position_side == PositionSide.LONG:
- self.sell(connector_name=connector_name,
- trading_pair=position.trading_pair,
- amount=abs(position.amount),
- order_type=OrderType.MARKET,
- price=connector.get_mid_price(position.trading_pair),
- position_action=PositionAction.CLOSE)
- elif position.position_side == PositionSide.SHORT:
- self.buy(connector_name=connector_name,
- trading_pair=position.trading_pair,
- amount=abs(position.amount),
- order_type=OrderType.MARKET,
- price=connector.get_mid_price(position.trading_pair),
- position_action=PositionAction.CLOSE)
-
- def on_tick(self):
- """
- This shows you how you can start meta controllers. You can run more than one at the same time and based on the
- market conditions, you can orchestrate from this script when to stop or start them.
- """
- for executor_handler in self.executor_handlers.values():
- if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED:
- executor_handler.start()
-
- def format_status(self) -> str:
- if not self.ready_to_trade:
- return "Market connectors are not ready."
- lines = []
- for trading_pair, executor_handler in self.executor_handlers.items():
- lines.extend(
- [f"Strategy: {executor_handler.controller.config.strategy_name} | Trading Pair: {trading_pair}",
- executor_handler.to_format_status()])
- return "\n".join(lines)
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_v2_multiple_pairs.py b/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_v2_multiple_pairs.py
deleted file mode 100644
index a3d7af2..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_v2_multiple_pairs.py
+++ /dev/null
@@ -1,139 +0,0 @@
-from decimal import Decimal
-from typing import Dict
-
-from hummingbot.connector.connector_base import ConnectorBase
-from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide
-from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
-from hummingbot.smart_components.controllers.dman_v2 import DManV2, DManV2Config
-from hummingbot.smart_components.strategy_frameworks.data_types import ExecutorHandlerStatus, TripleBarrierConf
-from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import (
- MarketMakingExecutorHandler,
-)
-from hummingbot.smart_components.utils.distributions import Distributions
-from hummingbot.smart_components.utils.order_level_builder import OrderLevelBuilder
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class DManV2MultiplePairs(ScriptStrategyBase):
- # Account configuration
- exchange = "binance_perpetual"
- trading_pairs = ["ETH-USDT"]
- leverage = 20
-
- # Candles configuration
- candles_exchange = "binance_perpetual"
- candles_interval = "3m"
- candles_max_records = 300
-
- # Orders configuration
- order_amount = Decimal("25")
- n_levels = 5
- start_spread = 0.0006
- step_between_orders = 0.009
- order_refresh_time = 60 * 15 # 15 minutes
- cooldown_time = 5
-
- # Triple barrier configuration
- stop_loss = Decimal("0.2")
- take_profit = Decimal("0.06")
- time_limit = 60 * 60 * 12
- trailing_stop_activation_price_delta = Decimal(str(step_between_orders / 2))
- trailing_stop_trailing_delta = Decimal(str(step_between_orders / 3))
-
- # Advanced configurations
- macd_fast = 12
- macd_slow = 26
- macd_signal = 9
- natr_length = 100
-
- # Applying the configuration
- order_level_builder = OrderLevelBuilder(n_levels=n_levels)
- order_levels = order_level_builder.build_order_levels(
- amounts=order_amount,
- spreads=Distributions.arithmetic(n_levels=n_levels, start=start_spread, step=step_between_orders),
- triple_barrier_confs=TripleBarrierConf(
- stop_loss=stop_loss, take_profit=take_profit, time_limit=time_limit,
- trailing_stop_activation_price_delta=trailing_stop_activation_price_delta,
- trailing_stop_trailing_delta=trailing_stop_trailing_delta),
- order_refresh_time=order_refresh_time,
- cooldown_time=cooldown_time,
- )
- controllers = {}
- markets = {}
- executor_handlers = {}
-
- for trading_pair in trading_pairs:
- config = DManV2Config(
- exchange=exchange,
- trading_pair=trading_pair,
- order_levels=order_levels,
- candles_config=[
- CandlesConfig(connector=candles_exchange, trading_pair=trading_pair,
- interval=candles_interval, max_records=candles_max_records),
- ],
- leverage=leverage,
- macd_fast=macd_fast,
- macd_slow=macd_slow,
- macd_signal=macd_signal,
- natr_length=natr_length,
- )
- controller = DManV2(config=config)
- markets = controller.update_strategy_markets_dict(markets)
- controllers[trading_pair] = controller
-
- def __init__(self, connectors: Dict[str, ConnectorBase]):
- super().__init__(connectors)
- for trading_pair, controller in self.controllers.items():
- self.executor_handlers[trading_pair] = MarketMakingExecutorHandler(strategy=self, controller=controller)
-
- @property
- def is_perpetual(self):
- """
- Checks if the exchange is a perpetual market.
- """
- return "perpetual" in self.exchange
-
- def on_stop(self):
- if self.is_perpetual:
- self.close_open_positions()
- for executor_handler in self.executor_handlers.values():
- executor_handler.stop()
-
- def close_open_positions(self):
- # we are going to close all the open positions when the bot stops
- for connector_name, connector in self.connectors.items():
- for trading_pair, position in connector.account_positions.items():
- if trading_pair in self.markets[connector_name]:
- if position.position_side == PositionSide.LONG:
- self.sell(connector_name=connector_name,
- trading_pair=position.trading_pair,
- amount=abs(position.amount),
- order_type=OrderType.MARKET,
- price=connector.get_mid_price(position.trading_pair),
- position_action=PositionAction.CLOSE)
- elif position.position_side == PositionSide.SHORT:
- self.buy(connector_name=connector_name,
- trading_pair=position.trading_pair,
- amount=abs(position.amount),
- order_type=OrderType.MARKET,
- price=connector.get_mid_price(position.trading_pair),
- position_action=PositionAction.CLOSE)
-
- def on_tick(self):
- """
- This shows you how you can start meta controllers. You can run more than one at the same time and based on the
- market conditions, you can orchestrate from this script when to stop or start them.
- """
- for executor_handler in self.executor_handlers.values():
- if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED:
- executor_handler.start()
-
- def format_status(self) -> str:
- if not self.ready_to_trade:
- return "Market connectors are not ready."
- lines = []
- for trading_pair, executor_handler in self.executor_handlers.items():
- lines.extend(
- [f"Strategy: {executor_handler.controller.config.strategy_name} | Trading Pair: {trading_pair}",
- executor_handler.to_format_status()])
- return "\n".join(lines)
diff --git a/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_v3_multiple_pairs.py b/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_v3_multiple_pairs.py
deleted file mode 100644
index dd58f6c..0000000
--- a/hummingbot_files/templates/master_bot_conf/scripts/v2_market-making_dman_v3_multiple_pairs.py
+++ /dev/null
@@ -1,141 +0,0 @@
-from decimal import Decimal
-from typing import Dict
-
-from hummingbot.connector.connector_base import ConnectorBase
-from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide
-from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
-from hummingbot.smart_components.controllers.dman_v3 import DManV3, DManV3Config
-from hummingbot.smart_components.strategy_frameworks.data_types import ExecutorHandlerStatus, TripleBarrierConf
-from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import (
- MarketMakingExecutorHandler,
-)
-from hummingbot.smart_components.utils.distributions import Distributions
-from hummingbot.smart_components.utils.order_level_builder import OrderLevelBuilder
-from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
-
-
-class DManV3MultiplePairs(ScriptStrategyBase):
- # Account configuration
- exchange = "binance_perpetual"
- trading_pairs = ["ETH-USDT"]
- leverage = 20
-
- # Candles configuration
- candles_exchange = "binance_perpetual"
- candles_interval = "1h"
- candles_max_records = 300
- bollinger_band_length = 200
- bollinger_band_std = 3.0
-
- # Orders configuration
- order_amount = Decimal("25")
- n_levels = 5
- start_spread = 0.5 # percentage of the bollinger band (0.5 means that the order will be between the bollinger mid-price and the upper band)
- step_between_orders = 0.3 # percentage of the bollinger band (0.1 means that the next order will be 10% of the bollinger band away from the previous order)
-
- # Triple barrier configuration
- stop_loss = Decimal("0.01")
- take_profit = Decimal("0.03")
- time_limit = 60 * 60 * 6
- trailing_stop_activation_price_delta = Decimal("0.008")
- trailing_stop_trailing_delta = Decimal("0.004")
-
- # Advanced configurations
- side_filter = True
- dynamic_spread_factor = True
- dynamic_target_spread = False
- smart_activation = False
- activation_threshold = Decimal("0.001")
-
- # Applying the configuration
- order_level_builder = OrderLevelBuilder(n_levels=n_levels)
- order_levels = order_level_builder.build_order_levels(
- amounts=order_amount,
- spreads=Distributions.arithmetic(n_levels=n_levels, start=start_spread, step=step_between_orders),
- triple_barrier_confs=TripleBarrierConf(
- stop_loss=stop_loss, take_profit=take_profit, time_limit=time_limit,
- trailing_stop_activation_price_delta=trailing_stop_activation_price_delta,
- trailing_stop_trailing_delta=trailing_stop_trailing_delta),
- )
- controllers = {}
- markets = {}
- executor_handlers = {}
-
- for trading_pair in trading_pairs:
- config = DManV3Config(
- exchange=exchange,
- trading_pair=trading_pair,
- order_levels=order_levels,
- candles_config=[
- CandlesConfig(connector=candles_exchange, trading_pair=trading_pair,
- interval=candles_interval, max_records=candles_max_records),
- ],
- bb_length=bollinger_band_length,
- bb_std=bollinger_band_std,
- side_filter=side_filter,
- dynamic_spread_factor=dynamic_spread_factor,
- dynamic_target_spread=dynamic_target_spread,
- smart_activation=smart_activation,
- activation_threshold=activation_threshold,
- leverage=leverage,
- )
- controller = DManV3(config=config)
- markets = controller.update_strategy_markets_dict(markets)
- controllers[trading_pair] = controller
-
- def __init__(self, connectors: Dict[str, ConnectorBase]):
- super().__init__(connectors)
- for trading_pair, controller in self.controllers.items():
- self.executor_handlers[trading_pair] = MarketMakingExecutorHandler(strategy=self, controller=controller)
-
- @property
- def is_perpetual(self):
- """
- Checks if the exchange is a perpetual market.
- """
- return "perpetual" in self.exchange
-
- def on_stop(self):
- if self.is_perpetual:
- self.close_open_positions()
- for executor_handler in self.executor_handlers.values():
- executor_handler.stop()
-
- def close_open_positions(self):
- # we are going to close all the open positions when the bot stops
- for connector_name, connector in self.connectors.items():
- for trading_pair, position in connector.account_positions.items():
- if trading_pair in self.markets[connector_name]:
- if position.position_side == PositionSide.LONG:
- self.sell(connector_name=connector_name,
- trading_pair=position.trading_pair,
- amount=abs(position.amount),
- order_type=OrderType.MARKET,
- price=connector.get_mid_price(position.trading_pair),
- position_action=PositionAction.CLOSE)
- elif position.position_side == PositionSide.SHORT:
- self.buy(connector_name=connector_name,
- trading_pair=position.trading_pair,
- amount=abs(position.amount),
- order_type=OrderType.MARKET,
- price=connector.get_mid_price(position.trading_pair),
- position_action=PositionAction.CLOSE)
-
- def on_tick(self):
- """
- This shows you how you can start meta controllers. You can run more than one at the same time and based on the
- market conditions, you can orchestrate from this script when to stop or start them.
- """
- for executor_handler in self.executor_handlers.values():
- if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED:
- executor_handler.start()
-
- def format_status(self) -> str:
- if not self.ready_to_trade:
- return "Market connectors are not ready."
- lines = []
- for trading_pair, executor_handler in self.executor_handlers.items():
- lines.extend(
- [f"Strategy: {executor_handler.controller.config.strategy_name} | Trading Pair: {trading_pair}",
- executor_handler.to_format_status()])
- return "\n".join(lines)
diff --git a/main.py b/main.py
index cebdbb8..218827f 100644
--- a/main.py
+++ b/main.py
@@ -3,7 +3,7 @@ from st_pages import Page, Section, show_pages
from streamlit_authenticator import Authenticate
from CONFIG import AUTH_SYSTEM_ENABLED
-from utils.os_utils import read_yaml_file, dump_dict_to_yaml
+from backend.utils.os_utils import read_yaml_file, dump_dict_to_yaml
def main_page():
@@ -11,21 +11,30 @@ def main_page():
[
Page("main.py", "Hummingbot Dashboard", "π"),
Section("Bot Orchestration", "π"),
- Page("pages/master_conf/app.py", "Credentials", "ποΈ"),
- Page("pages/bot_orchestration/app.py", "Instances", "π¦
"),
- Page("pages/file_manager/app.py", "File Explorer", "π"),
- Page("pages/position_builder/app.py", "Position Builder", "π"),
- Section("Backtest Manager", "βοΈ"),
- Page("pages/backtest_get_data/app.py", "Get Data", "πΎ"),
- Page("pages/backtest_create/create.py", "Create", "βοΈ"),
- Page("pages/backtest_optimize/optimize.py", "Optimize", "π§ͺ"),
- Page("pages/backtest_analyze/analyze.py", "Analyze", "π¬"),
- Page("pages/launch_bot/app.py", "Deploy", "π"),
+ Page("frontend/pages/orchestration/instances/app.py", "Instances", "π¦
"),
+ Page("frontend/pages/orchestration/launch_bot_v2/app.py", "Deploy", "π"),
+ Page("frontend/pages/orchestration/credentials/app.py", "Credentials", "π"),
+ Page("frontend/pages/orchestration/portfolio/app.py", "Portfolio", "π°"),
+ # Page("frontend/pages/orchestration/launch_bot_v2_st/app.py", "Deploy ST", "π"),
+ # Page("pages/file_manager/app.py", "File Explorer", "π"),
+ Section("Config Generator", "ποΈ"),
+ Page("frontend/pages/config/pmm_simple/app.py", "PMM Simple", "π¨βπ«"),
+ Page("frontend/pages/config/pmm_dynamic/app.py", "PMM Dynamic", "π©βπ«"),
+ Page("frontend/pages/config/dman_maker_v2/app.py", "D-Man Maker V2", "π€"),
+ Page("frontend/pages/config/bollinger_v1/app.py", "Bollinger V1", "π"),
+ Page("frontend/pages/config/macd_bb_v1/app.py", "MACD_BB V1", "π"),
+ Page("frontend/pages/config/supertrend_v1/app.py", "SuperTrend V1", "π¨βπ¬"),
+ Page("frontend/pages/config/xemm_controller/app.py", "XEMM Controller", "β‘οΈ"),
+ # Page("frontend/pages/config/position_builder/app.py", "Position Builder", "π"),
+ Section("Data", "πΎ"),
+ Page("frontend/pages/data/download_candles/app.py", "Download Candles", "πΉ"),
+ # Page("pages/create/create.py", "Create", "βοΈ"),
+ # Page("pages/optimize/optimize.py", "Optimize", "π§ͺ"),
+ # Page("pages/analyze/analyze.py", "Analyze", "π¬"),
Section("Community Pages", "π¨βπ©βπ§βπ¦"),
- Page("pages/strategy_performance/app.py", "Strategy Performance", "π"),
- Page("pages/db_inspector/app.py", "DB Inspector", "π"),
- Page("pages/token_spreads/app.py", "Token Spreads", "π§"),
- Page("pages/tvl_vs_mcap/app.py", "TVL vs Market Cap", "π¦"),
+ # Page("frontend/pages/performance/strategy_performance/app.py", "Strategy Performance", "π"),
+ Page("frontend/pages/data/token_spreads/app.py", "Token Spreads", "π§"),
+ Page("frontend/pages/data/tvl_vs_mcap/app.py", "TVL vs Market Cap", "π¦"),
]
)
@@ -86,42 +95,40 @@ def main_page():
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("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).")
+ 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).")
-config = read_yaml_file("credentials.yml")
+# config = read_yaml_file("credentials.yml")
+#
+# if "authenticator" not in st.session_state:
+# st.session_state.authenticator = Authenticate(
+# config['credentials'],
+# config['cookie']['name'],
+# config['cookie']['key'],
+# config['cookie']['expiry_days'],
+# config['preauthorized']
+# )
-if "authenticator" not in st.session_state:
- st.session_state.authenticator = Authenticate(
- config['credentials'],
- config['cookie']['name'],
- config['cookie']['key'],
- config['cookie']['expiry_days'],
- config['preauthorized']
- )
-
-if not AUTH_SYSTEM_ENABLED:
- main_page()
-elif st.session_state["authentication_status"]:
- config["credentials"] = st.session_state.authenticator.credentials
- dump_dict_to_yaml(config, "credentials.yml")
- with st.sidebar:
- st.write(f'Welcome {st.session_state["name"]}!')
- st.session_state.authenticator.logout(location='sidebar') # Updated logout call
- main_page()
-else:
- show_pages([
- Page("main.py", "Hummingbot Dashboard", "π"),
- ])
- name, authentication_status, username = st.session_state.authenticator.login(location='main') # Updated login call
- if st.session_state["authentication_status"] == False:
- st.error('Username/password is incorrect')
- elif st.session_state["authentication_status"] == None:
- st.warning('Please enter your username and password')
- st.write("---")
- st.write("If you are pre-authorized, you can login with your pre-authorized mail!")
- st.session_state.authenticator.register_user(location='main') # Updated register user call
+# if not AUTH_SYSTEM_ENABLED:
+main_page()
+# elif st.session_state["authentication_status"]:
+# config["credentials"] = st.session_state.authenticator_handler.credentials
+# dump_dict_to_yaml(config, "credentials.yml")
+# with st.sidebar:
+# st.write(f'Welcome {st.session_state["name"]}!')
+# st.session_state.authenticator.logout(location='sidebar') # Updated logout call
+# main_page()
+# else:
+# show_pages([
+# Page("main.py", "Hummingbot Dashboard", "π"),
+# ])
+# name, authentication_status, username = st.session_state.authenticator.login(location='main') # Updated login call
+# if st.session_state["authentication_status"] == False:
+# st.error('Username/password is incorrect')
+# elif st.session_state["authentication_status"] == None:
+# st.warning('Please enter your username and password')
+# st.write("---")
+# st.write("If you are pre-authorized, you can login with your pre-authorized mail!")
+# st.session_state.authenticator.register_user(location='main') # Updated register user call
diff --git a/pages/backtest_get_data/app.py b/pages/backtest_get_data/app.py
deleted file mode 100644
index 4239b60..0000000
--- a/pages/backtest_get_data/app.py
+++ /dev/null
@@ -1,70 +0,0 @@
-import time
-from subprocess import CalledProcessError
-
-import streamlit as st
-
-import constants
-from utils import os_utils
-from docker_manager import DockerManager
-
-from utils.st_utils import initialize_st_page
-
-
-initialize_st_page(title="Get Data", icon="πΎ", initial_sidebar_state="collapsed")
-
-# Start content here
-docker_manager = DockerManager()
-
-c1, c2, c3 = st.columns([2, 2, 0.5])
-with c1:
- exchange = st.selectbox("Exchange", ["binance_perpetual", "binance"], index=0)
- trading_pairs = st.text_input("Trading Pairs (separated with commas)", value="BTC-USDT,ETH-USDT")
-with c2:
- intervals = st.multiselect("Intervals", options=["1s", "1m", "3m", "5m", "15m", "1h", "4h", "1d"], default=["1m", "3m", "1h"])
- days_to_download = st.number_input("Days to Download", value=30, min_value=1, max_value=365, step=1)
-with c3:
- get_data_button = st.button("Download Candles!")
- clean_container_folder_button = st.button("Clean Candles Folder")
-
-if clean_container_folder_button:
- st.warning("Cleaning Candles Data folder...", icon="β οΈ")
- st.write("---")
- os_utils.remove_files_from_directory(constants.CANDLES_DATA_PATH)
- st.write("### Container folder cleaned.")
- st.write("---")
-
-if get_data_button:
- candles_container_config = {
- "EXCHANGE": exchange,
- "TRADING_PAIRS": trading_pairs,
- "INTERVALS": ",".join(intervals),
- "DAYS_TO_DOWNLOAD": days_to_download,
- }
- time.sleep(0.5)
- docker_manager.create_download_candles_container(candles_config=candles_container_config,
- yml_path=constants.DOWNLOAD_CANDLES_CONFIG_YML)
- st.info("Downloading candles with a Docker container in the background. "
- "When this process is ready you will see the candles inside data/candles", icon="π")
-
-st.write("---")
-st.write("## βοΈContainers Management")
-try:
- active_containers = docker_manager.get_active_containers()
- c1, c2 = st.columns([0.9, 0.1])
- with c1:
- if "backtest_get_data" in active_containers:
- st.success("Hummingbot Candles Downloader is running")
- st.write("Exited Containers:")
- st.warning(docker_manager.get_exited_containers())
- with c2:
- if "backtest_get_data" in active_containers:
- stop_containers_button = st.button("Stop Candles Downloader")
- if stop_containers_button:
- docker_manager.stop_container("backtest_get_data")
- clean_exited_containers_button = st.button("Clean Containers")
- if clean_exited_containers_button:
- docker_manager.clean_exited_containers()
-
-except CalledProcessError as error:
- st.write("### Docker is not running. Please start docker in your machine.")
-
diff --git a/pages/bot_orchestration/app.py b/pages/bot_orchestration/app.py
deleted file mode 100644
index 00f9499..0000000
--- a/pages/bot_orchestration/app.py
+++ /dev/null
@@ -1,156 +0,0 @@
-from types import SimpleNamespace
-
-from commlib.exceptions import RPCClientTimeoutError
-
-import constants
-import streamlit as st
-from streamlit_elements import elements, mui, lazy, sync, event
-import time
-
-from docker_manager import DockerManager
-from hbotrc import BotCommands
-
-from ui_components.bot_performance_card import BotPerformanceCard
-from ui_components.dashboard import Dashboard
-from ui_components.exited_bot_card import ExitedBotCard
-from ui_components.launch_bot_card import LaunchBotCard
-from ui_components.launch_broker_card import LaunchBrokerCard
-from utils.st_utils import initialize_st_page
-
-CARD_WIDTH = 6
-CARD_HEIGHT = 3
-NUM_CARD_COLS = 2
-
-initialize_st_page(title="Instances", icon="π¦
", initial_sidebar_state="collapsed")
-
-if "is_broker_running" not in st.session_state:
- st.session_state.is_broker_running = False
-
-if "active_bots" not in st.session_state:
- st.session_state.active_bots = {}
-
-if "exited_bots" not in st.session_state:
- st.session_state.exited_bots = {}
-
-if "new_bot_name" not in st.session_state:
- st.session_state.new_bot_name = ""
-
-if "selected_strategy" not in st.session_state:
- st.session_state.selected_strategy = None
-
-if "editor_tabs" not in st.session_state:
- st.session_state.editor_tabs = {}
-
-
-def update_containers_info(docker_manager):
- active_containers = docker_manager.get_active_containers()
- st.session_state.is_broker_running = "hummingbot-broker" in active_containers
- if st.session_state.is_broker_running:
- try:
- active_hbot_containers = [container for container in active_containers if
- "hummingbot-" in container and "broker" not in container
- and "master_bot_conf" not in container]
- previous_active_bots = list(st.session_state.active_bots)
-
- # Remove bots that are no longer active
- for bot in previous_active_bots:
- if bot not in active_hbot_containers:
- del st.session_state.active_bots[bot]
-
- # Add new bots
- for bot in active_hbot_containers:
- if bot not in previous_active_bots:
- st.session_state.active_bots[bot] = {
- "bot_name": bot,
- "broker_client": BotCommands(host='localhost', port=1883, username='admin', password='password',
- bot_id=bot)
- }
-
- # Update bot info
- for bot in list(st.session_state.active_bots):
- try:
- broker_client = st.session_state.active_bots[bot]["broker_client"]
- status = broker_client.status()
- history = broker_client.history()
- is_running = "No strategy is currently running" not in status.msg
- st.session_state.active_bots[bot]["is_running"] = is_running
- st.session_state.active_bots[bot]["status"] = status.msg
- st.session_state.active_bots[bot]["trades"] = history.trades
- st.session_state.active_bots[bot]["selected_strategy"] = None
- except RPCClientTimeoutError:
- st.error(f"RPCClientTimeoutError: Could not connect to {bot}. Please review the connection.")
- del st.session_state.active_bots[bot]
- except RuntimeError:
- st.rerun()
- st.session_state.active_bots = dict(
- sorted(st.session_state.active_bots.items(), key=lambda x: x[1]['is_running'], reverse=True))
- else:
- st.session_state.active_bots = {}
-
-
-docker_manager = DockerManager()
-if not docker_manager.is_docker_running():
- st.warning("Docker is not running. Please start Docker and refresh the page.")
- st.stop()
-update_containers_info(docker_manager)
-exited_containers = [container for container in docker_manager.get_exited_containers() if "broker" not in container]
-
-
-def get_grid_positions(n_cards: int, cols: int = NUM_CARD_COLS, card_width: int = CARD_HEIGHT, card_height: int = CARD_WIDTH):
- rows = n_cards // cols + 1
- x_y = [(x * card_width, y * card_height) for x in range(cols) for y in range(rows)]
- return sorted(x_y, key=lambda x: (x[1], x[0]))
-
-
-if "create_containers_board" not in st.session_state:
- board = Dashboard()
- create_containers_board = SimpleNamespace(
- dashboard=board,
- launch_bot=LaunchBotCard(board, 0, 0, 8, 1.5),
- launch_broker=LaunchBrokerCard(board, 8, 0, 4, 1.5)
- )
- st.session_state.create_containers_board = create_containers_board
-
-else:
- create_containers_board = st.session_state.create_containers_board
-
-
-with elements("create_bot"):
- with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True):
- with create_containers_board.dashboard():
- create_containers_board.launch_bot()
- create_containers_board.launch_broker()
-
-
-with elements("active_instances_board"):
- with mui.Paper(sx={"padding": "2rem"}, variant="outlined"):
- mui.Typography("π¦
Active Instances", variant="h5")
- if st.session_state.is_broker_running:
- quantity_of_active_bots = len(st.session_state.active_bots)
- if quantity_of_active_bots > 0:
- # TODO: Make layout configurable
- grid_positions = get_grid_positions(n_cards=quantity_of_active_bots, cols=NUM_CARD_COLS,
- card_width=CARD_WIDTH, card_height=CARD_HEIGHT)
- active_instances_board = Dashboard()
- for (bot, config), (x, y) in zip(st.session_state.active_bots.items(), grid_positions):
- st.session_state.active_bots[bot]["bot_performance_card"] = BotPerformanceCard(active_instances_board,
- x, y,
- CARD_WIDTH, CARD_HEIGHT)
- with active_instances_board():
- for bot, config in st.session_state.active_bots.items():
- st.session_state.active_bots[bot]["bot_performance_card"](config)
- else:
- mui.Alert("No active bots found. Please create a new bot.", severity="info", sx={"margin": "1rem"})
- else:
- mui.Alert("Please start Hummingbot Broker to control your bots.", severity="warning", sx={"margin": "1rem"})
-with elements("stopped_instances_board"):
- grid_positions = get_grid_positions(n_cards=len(exited_containers), cols=NUM_CARD_COLS, card_width=CARD_WIDTH, card_height=CARD_HEIGHT)
- exited_instances_board = Dashboard()
- for exited_instance, (x, y) in zip(exited_containers, grid_positions):
- st.session_state.exited_bots[exited_instance] = ExitedBotCard(exited_instances_board, x, y,
- CARD_WIDTH, 1)
- with mui.Paper(style={"padding": "2rem"}, variant="outlined"):
- mui.Typography("π€ Stopped Instances", variant="h5")
- with exited_instances_board():
- for bot, card in st.session_state.exited_bots.items():
- card(bot)
diff --git a/pages/launch_bot/app.py b/pages/launch_bot/app.py
deleted file mode 100644
index dd2e43c..0000000
--- a/pages/launch_bot/app.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from types import SimpleNamespace
-
-import streamlit as st
-from streamlit_elements import elements, mui
-
-from docker_manager import DockerManager
-
-from ui_components.dashboard import Dashboard
-from ui_components.launch_strategy_v2 import LaunchStrategyV2
-from utils.st_utils import initialize_st_page
-
-CARD_WIDTH = 6
-CARD_HEIGHT = 3
-NUM_CARD_COLS = 2
-
-initialize_st_page(title="Launch Bot", icon="π", initial_sidebar_state="collapsed")
-
-
-docker_manager = DockerManager()
-if not docker_manager.is_docker_running():
- st.warning("Docker is not running. Please start Docker and refresh the page.")
- st.stop()
-
-
-def get_grid_positions(n_cards: int, cols: int = NUM_CARD_COLS, card_width: int = CARD_HEIGHT, card_height: int = CARD_WIDTH):
- rows = n_cards // cols + 1
- x_y = [(x * card_width, y * card_height) for x in range(cols) for y in range(rows)]
- return sorted(x_y, key=lambda x: (x[1], x[0]))
-
-
-if "launch_bots_board" not in st.session_state:
- board = Dashboard()
- launch_bots_board = SimpleNamespace(
- dashboard=board,
- launch_bot=LaunchStrategyV2(board, 0, 0, 12, 10),
- )
- st.session_state.launch_bots_board = launch_bots_board
-
-else:
- launch_bots_board = st.session_state.launch_bots_board
-
-
-with elements("create_bot"):
- with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True):
- with launch_bots_board.dashboard():
- launch_bots_board.launch_bot()
diff --git a/pages/master_conf/app.py b/pages/master_conf/app.py
deleted file mode 100644
index 75727f4..0000000
--- a/pages/master_conf/app.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import glob
-import os
-from types import SimpleNamespace
-import streamlit as st
-from docker_manager import DockerManager
-from streamlit_elements import elements, mui
-
-import constants
-from ui_components.dashboard import Dashboard
-from ui_components.editor import Editor
-from ui_components.launch_master_bot_card import LaunchMasterBotCard
-from ui_components.master_conf_file_explorer import MasterConfFileExplorer
-from utils.st_utils import initialize_st_page
-
-
-initialize_st_page(title="Credentials", icon="ποΈ", initial_sidebar_state="collapsed")
-
-docker_manager = DockerManager()
-if not docker_manager.is_docker_running():
- st.warning("Docker is not running. Please start Docker and refresh the page.")
- st.stop()
-
-if "mc_board" not in st.session_state:
- board = Dashboard()
- mc_board = SimpleNamespace(
- dashboard=board,
- launch_master_bot=LaunchMasterBotCard(board, 0, 0, 12, 2),
- file_explorer=MasterConfFileExplorer(board, 0, 4, 3, 7),
- editor=Editor(board, 4, 4, 9, 7),
- )
- st.session_state.mc_board = mc_board
-
-else:
- mc_board = st.session_state.mc_board
-
-# Add new tabs
-for tab_name, content in mc_board.file_explorer.tabs.items():
- if tab_name not in mc_board.editor.tabs:
- mc_board.editor.add_tab(tab_name, content["content"], content["language"])
-
-# Remove deleted tabs
-for tab_name in list(mc_board.editor.tabs.keys()):
- if tab_name not in mc_board.file_explorer.tabs:
- mc_board.editor.remove_tab(tab_name)
-
-
-
-with elements("file_manager"):
- with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True):
- with mc_board.dashboard():
- mc_board.launch_master_bot()
- mc_board.file_explorer()
- mc_board.editor()
diff --git a/pages/reference_data/7_π_Data.py b/pages/reference_data/7_π_Data.py
deleted file mode 100644
index c75547e..0000000
--- a/pages/reference_data/7_π_Data.py
+++ /dev/null
@@ -1,74 +0,0 @@
-import streamlit as st
-
-import CONFIG
-from utils.coingecko_utils import CoinGeckoUtils
-from utils.miner_utils import MinerUtils
-from utils.st_utils import initialize_st_page
-
-
-@st.cache_data
-def get_all_coins_df():
- return CoinGeckoUtils().get_all_coins_df()
-
-
-@st.cache_data
-def get_all_exchanges_df():
- return CoinGeckoUtils().get_all_exchanges_df()
-
-
-@st.cache_data
-def get_miner_stats_df():
- return MinerUtils().get_miner_stats_df()
-
-
-@st.cache_data
-def get_coin_tickers_by_id_list(coins_id: list):
- return CoinGeckoUtils().get_coin_tickers_by_id_list(coins_id)
-
-
-initialize_st_page(title="Crypto Data", icon=":bar_chart:")
-
-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()
-
- data = st.container()
- with data:
- data.write("Data loaded successfully!")
-
-miner_coins = coins_df.loc[coins_df["symbol"].isin(miner_stats_df["base"].str.lower().unique()), "name"]
-default_miner_coins = ["Avalanche"]
-
-st.write("---")
-st.write("## Exchanges and coins data")
-
-with st.expander('Coins data'):
- st.dataframe(coins_df)
-
-with st.expander('Exchanges data'):
- st.dataframe(exchanges_df)
-
-st.write("---")
-st.write("## Tickers filtered")
-
-st.write("### Coins filter π¦
")
-tokens = st.multiselect(
- "Select the tokens to analyze:",
- options=coins_df["name"],
- default=default_miner_coins)
-
-coins_id = coins_df.loc[coins_df["name"].isin(tokens), "id"].tolist()
-
-with st.spinner(text='Loading coin tickers data...'):
- 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)
-
-st.write("### Exchanges filter π¦
")
-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()])
-
-with st.expander('Coins Tickers Data'):
- st.dataframe(coin_tickers_df)
diff --git a/quants_lab/controllers/bollinger_v1.py b/quants_lab/controllers/bollinger_v1.py
deleted file mode 100644
index 30c1372..0000000
--- a/quants_lab/controllers/bollinger_v1.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import time
-
-import pandas as pd
-import pandas_ta as ta
-from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
-from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
-from hummingbot.smart_components.strategy_frameworks.directional_trading import DirectionalTradingControllerConfigBase, \
- DirectionalTradingControllerBase
-from pydantic import Field
-
-
-class BollingerV1Conf(DirectionalTradingControllerConfigBase):
- strategy_name = "bollinger_v1"
- bb_length: int = Field(default=100, ge=100, le=400)
- bb_std: float = Field(default=2.0, ge=2.0, le=3.0)
- bb_long_threshold: float = Field(default=0.0, ge=-1.0, le=0.2)
- bb_short_threshold: float = Field(default=1.0, ge=0.8, le=2.0)
-
-
-class BollingerV1(DirectionalTradingControllerBase):
-
- def __init__(self, config: BollingerV1Conf):
- 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.
- """
- 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
-
- # Add indicators
- df.ta.bbands(length=self.config.bb_length, std=self.config.bb_std, append=True)
-
- # Generate signal
- long_condition = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}"] < self.config.bb_long_threshold
- short_condition = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}"] > self.config.bb_short_threshold
-
- # Generate signal
- df["signal"] = 0
- df.loc[long_condition, "signal"] = 1
- df.loc[short_condition, "signal"] = -1
- return df
-
- def extra_columns_to_show(self):
- return [f"BBP_{self.config.bb_length}_{self.config.bb_std}"]
\ No newline at end of file
diff --git a/quants_lab/controllers/dman_v1.py b/quants_lab/controllers/dman_v1.py
deleted file mode 100644
index 782e055..0000000
--- a/quants_lab/controllers/dman_v1.py
+++ /dev/null
@@ -1,101 +0,0 @@
-import time
-from decimal import Decimal
-
-import pandas_ta as ta # noqa: F401
-
-from hummingbot.core.data_type.common import TradeType
-from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop
-from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
-from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
-from hummingbot.smart_components.strategy_frameworks.market_making.market_making_controller_base import (
- MarketMakingControllerBase,
- MarketMakingControllerConfigBase,
-)
-
-
-class DManV1Config(MarketMakingControllerConfigBase):
- strategy_name: str = "dman_v1"
- natr_length: int = 14
-
-
-class DManV1(MarketMakingControllerBase):
- """
- Directional Market Making Strategy making use of NATR indicator to make spreads dynamic.
- """
-
- def __init__(self, config: DManV1Config):
- super().__init__(config)
- self.config = config
-
- def refresh_order_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
- """
- Checks if the order needs to be refreshed.
- You can reimplement this method to add more conditions.
- """
- if executor.position_config.timestamp + order_level.order_refresh_time > time.time():
- return False
- return True
-
- 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.
- """
- 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):
- """
- Gets the price and spread multiplier from the last candlestick.
- """
- candles_df = self.candles[0].candles_df
- natr = ta.natr(candles_df["high"], candles_df["low"], candles_df["close"], length=self.config.natr_length) / 100
-
- candles_df["spread_multiplier"] = natr
- candles_df["price_multiplier"] = 0.0
- return candles_df
-
- def get_position_config(self, order_level: OrderLevel) -> PositionConfig:
- """
- Creates a PositionConfig object from an OrderLevel object.
- Here you can use technical indicators to determine the parameters of the position config.
- """
- close_price = self.get_close_price(self.config.trading_pair)
- price_multiplier, spread_multiplier = self.get_price_and_spread_multiplier()
-
- price_adjusted = close_price * (1 + price_multiplier)
- side_multiplier = -1 if order_level.side == TradeType.BUY else 1
- order_price = price_adjusted * (1 + order_level.spread_factor * spread_multiplier * side_multiplier)
- amount = order_level.order_amount_usd / order_price
-
- if order_level.triple_barrier_conf.trailing_stop_trailing_delta and order_level.triple_barrier_conf.trailing_stop_trailing_delta:
- trailing_stop = TrailingStop(
- activation_price_delta=order_level.triple_barrier_conf.trailing_stop_activation_price_delta,
- trailing_delta=order_level.triple_barrier_conf.trailing_stop_trailing_delta,
- )
- else:
- trailing_stop = None
- position_config = PositionConfig(
- timestamp=time.time(),
- trading_pair=self.config.trading_pair,
- exchange=self.config.exchange,
- side=order_level.side,
- amount=amount,
- take_profit=order_level.triple_barrier_conf.take_profit,
- stop_loss=order_level.triple_barrier_conf.stop_loss,
- time_limit=order_level.triple_barrier_conf.time_limit,
- entry_price=Decimal(order_price),
- open_order_type=order_level.triple_barrier_conf.open_order_type,
- take_profit_order_type=order_level.triple_barrier_conf.take_profit_order_type,
- trailing_stop=trailing_stop,
- leverage=self.config.leverage
- )
- return position_config
diff --git a/quants_lab/controllers/dman_v2.py b/quants_lab/controllers/dman_v2.py
deleted file mode 100644
index 402d479..0000000
--- a/quants_lab/controllers/dman_v2.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import time
-from decimal import Decimal
-
-import pandas_ta as ta # noqa: F401
-
-from hummingbot.core.data_type.common import TradeType
-from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop
-from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
-from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
-from hummingbot.smart_components.strategy_frameworks.market_making.market_making_controller_base import (
- MarketMakingControllerBase,
- MarketMakingControllerConfigBase,
-)
-
-
-class DManV2Config(MarketMakingControllerConfigBase):
- strategy_name: str = "dman_v2"
- macd_fast: int = 12
- macd_slow: int = 26
- macd_signal: int = 9
- natr_length: int = 14
-
-
-class DManV2(MarketMakingControllerBase):
- """
- Directional Market Making Strategy making use of NATR indicator to make spreads dynamic and shift the mid price.
- """
-
- def __init__(self, config: DManV2Config):
- super().__init__(config)
- self.config = config
-
- def refresh_order_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
- """
- Checks if the order needs to be refreshed.
- You can reimplement this method to add more conditions.
- """
- if executor.position_config.timestamp + order_level.order_refresh_time > time.time():
- return False
- return True
-
- 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.
- """
- 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):
- """
- Gets the price and spread multiplier from the last candlestick.
- """
- candles_df = self.candles[0].candles_df
- natr = ta.natr(candles_df["high"], candles_df["low"], candles_df["close"], length=self.config.natr_length) / 100
-
- macd_output = ta.macd(candles_df["close"], fast=self.config.macd_fast, slow=self.config.macd_slow, signal=self.config.macd_signal)
- macd = macd_output[f"MACD_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"]
- macdh = macd_output[f"MACDh_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"]
- macd_signal = - (macd - macd.mean()) / macd.std()
- macdh_signal = macdh.apply(lambda x: 1 if x > 0 else -1)
- max_price_shift = natr / 2
-
- price_multiplier = (0.5 * macd_signal + 0.5 * macdh_signal) * max_price_shift
-
- candles_df["spread_multiplier"] = natr
- candles_df["price_multiplier"] = price_multiplier
- return candles_df
-
- def get_position_config(self, order_level: OrderLevel) -> PositionConfig:
- """
- Creates a PositionConfig object from an OrderLevel object.
- Here you can use technical indicators to determine the parameters of the position config.
- """
- close_price = self.get_close_price(self.config.trading_pair)
- price_multiplier, spread_multiplier = self.get_price_and_spread_multiplier()
-
- price_adjusted = close_price * (1 + price_multiplier)
- side_multiplier = -1 if order_level.side == TradeType.BUY else 1
- order_price = price_adjusted * (1 + order_level.spread_factor * spread_multiplier * side_multiplier)
- amount = order_level.order_amount_usd / order_price
- if order_level.triple_barrier_conf.trailing_stop_trailing_delta and order_level.triple_barrier_conf.trailing_stop_trailing_delta:
- trailing_stop = TrailingStop(
- activation_price_delta=order_level.triple_barrier_conf.trailing_stop_activation_price_delta,
- trailing_delta=order_level.triple_barrier_conf.trailing_stop_trailing_delta,
- )
- else:
- trailing_stop = None
- position_config = PositionConfig(
- timestamp=time.time(),
- trading_pair=self.config.trading_pair,
- exchange=self.config.exchange,
- side=order_level.side,
- amount=amount,
- take_profit=order_level.triple_barrier_conf.take_profit,
- stop_loss=order_level.triple_barrier_conf.stop_loss,
- time_limit=order_level.triple_barrier_conf.time_limit,
- entry_price=Decimal(order_price),
- open_order_type=order_level.triple_barrier_conf.open_order_type,
- take_profit_order_type=order_level.triple_barrier_conf.take_profit_order_type,
- trailing_stop=trailing_stop,
- leverage=self.config.leverage
- )
- return position_config
diff --git a/quants_lab/controllers/dman_v3.py b/quants_lab/controllers/dman_v3.py
deleted file mode 100644
index caea6f2..0000000
--- a/quants_lab/controllers/dman_v3.py
+++ /dev/null
@@ -1,125 +0,0 @@
-import time
-from decimal import Decimal
-
-import pandas_ta as ta # noqa: F401
-
-from hummingbot.core.data_type.common import TradeType
-from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop
-from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
-from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
-from hummingbot.smart_components.strategy_frameworks.market_making.market_making_controller_base import (
- MarketMakingControllerBase,
- MarketMakingControllerConfigBase,
-)
-
-
-class DManV3Config(MarketMakingControllerConfigBase):
- strategy_name: str = "dman_v3"
- bb_length: int = 100
- bb_std: float = 2.0
- side_filter: bool = False
- smart_activation: bool = False
- activation_threshold: Decimal = Decimal("0.001")
- dynamic_spread_factor: bool = True
- dynamic_target_spread: bool = False
-
-
-class DManV3(MarketMakingControllerBase):
- """
- Mean reversion strategy with Grid execution making use of Bollinger Bands indicator to make spreads dynamic
- and shift the mid price.
- """
-
- def __init__(self, config: DManV3Config):
- super().__init__(config)
- self.config = config
-
- def refresh_order_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
- """
- Checks if the order needs to be refreshed.
- You can reimplement this method to add more conditions.
- """
- if executor.position_config.timestamp + order_level.order_refresh_time > time.time():
- return False
- return True
-
- 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.
- """
- 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):
- """
- Gets the price and spread multiplier from the last candlestick.
- """
- candles_df = self.candles[0].candles_df
- bbp = ta.bbands(candles_df["close"], length=self.config.bb_length, std=self.config.bb_std)
-
- candles_df["price_multiplier"] = bbp[f"BBM_{self.config.bb_length}_{self.config.bb_std}"]
- candles_df["spread_multiplier"] = bbp[f"BBB_{self.config.bb_length}_{self.config.bb_std}"] / 200
- return candles_df
-
- def get_position_config(self, order_level: OrderLevel) -> PositionConfig:
- """
- Creates a PositionConfig object from an OrderLevel object.
- Here you can use technical indicators to determine the parameters of the position config.
- """
- close_price = self.get_close_price(self.config.trading_pair)
-
- bollinger_mid_price, spread_multiplier = self.get_price_and_spread_multiplier()
- if not self.config.dynamic_spread_factor:
- spread_multiplier = 1
- side_multiplier = -1 if order_level.side == TradeType.BUY else 1
- order_spread_multiplier = order_level.spread_factor * spread_multiplier * side_multiplier
- order_price = bollinger_mid_price * (1 + order_spread_multiplier)
- amount = order_level.order_amount_usd / order_price
-
- # Avoid placing the order from the opposite side
- side_filter_condition = self.config.side_filter and (
- (bollinger_mid_price > close_price and side_multiplier == 1) or
- (bollinger_mid_price < close_price and side_multiplier == -1))
- if side_filter_condition:
- return
-
- # Smart activation of orders
- smart_activation_condition = self.config.smart_activation and (
- side_multiplier == 1 and (close_price < order_price * (1 + self.config.activation_threshold)) or
- (side_multiplier == -1 and (close_price > order_price * (1 - self.config.activation_threshold))))
- if smart_activation_condition:
- return
-
- target_spread = spread_multiplier if self.config.dynamic_target_spread else 1
- if order_level.triple_barrier_conf.trailing_stop_trailing_delta and order_level.triple_barrier_conf.trailing_stop_trailing_delta:
- trailing_stop = TrailingStop(
- activation_price_delta=order_level.triple_barrier_conf.trailing_stop_activation_price_delta * target_spread,
- trailing_delta=order_level.triple_barrier_conf.trailing_stop_trailing_delta * target_spread,
- )
- else:
- trailing_stop = None
- position_config = PositionConfig(
- timestamp=time.time(),
- trading_pair=self.config.trading_pair,
- exchange=self.config.exchange,
- side=order_level.side,
- amount=amount,
- take_profit=order_level.triple_barrier_conf.take_profit * target_spread,
- stop_loss=order_level.triple_barrier_conf.stop_loss * target_spread,
- time_limit=order_level.triple_barrier_conf.time_limit,
- entry_price=Decimal(order_price),
- open_order_type=order_level.triple_barrier_conf.open_order_type,
- take_profit_order_type=order_level.triple_barrier_conf.take_profit_order_type,
- trailing_stop=trailing_stop,
- leverage=self.config.leverage
- )
- return position_config
diff --git a/quants_lab/controllers/macd_bb_v1.py b/quants_lab/controllers/macd_bb_v1.py
deleted file mode 100644
index 834362f..0000000
--- a/quants_lab/controllers/macd_bb_v1.py
+++ /dev/null
@@ -1,72 +0,0 @@
-import time
-from typing import Optional
-
-import pandas as pd
-from pydantic import Field
-
-from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
-from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
-from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_controller_base import (
- DirectionalTradingControllerBase,
- DirectionalTradingControllerConfigBase,
-)
-
-
-class MACDBBV1Config(DirectionalTradingControllerConfigBase):
- strategy_name: str = "macd_bb_v1"
- bb_length: int = Field(default=24, ge=100, le=200)
- bb_std: float = Field(default=2.0, ge=2.0, le=3.0)
- bb_long_threshold: float = Field(default=0.0, ge=-1.0, le=0.2)
- bb_short_threshold: float = Field(default=1.0, ge=0.8, le=2.0)
- macd_fast: int = Field(default=21, ge=12, le=60)
- macd_slow: int = Field(default=42, ge=26, le=200)
- macd_signal: int = Field(default=9, ge=8, le=20)
-
-
-class MACDBBV1(DirectionalTradingControllerBase):
- """
- Directional Market Making Strategy making use of NATR indicator to make spreads dynamic.
- """
-
- def __init__(self, config: MACDBBV1Config):
- 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.
- """
- 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
-
- # Add indicators
- df.ta.bbands(length=self.config.bb_length, std=self.config.bb_std, append=True)
- df.ta.macd(fast=self.config.macd_fast, slow=self.config.macd_slow, signal=self.config.macd_signal, append=True)
- bbp = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}"]
- macdh = df[f"MACDh_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"]
- macd = df[f"MACD_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"]
-
- # Generate signal
- long_condition = (bbp < self.config.bb_long_threshold) & (macdh > 0) & (macd < 0)
- short_condition = (bbp > self.config.bb_short_threshold) & (macdh < 0) & (macd > 0)
- df["signal"] = 0
- df.loc[long_condition, "signal"] = 1
- df.loc[short_condition, "signal"] = -1
- return df
-
- def extra_columns_to_show(self):
- return [f"BBP_{self.config.bb_length}_{self.config.bb_std}",
- f"MACDh_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}",
- f"MACD_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"]
diff --git a/quants_lab/controllers/supertrend.py b/quants_lab/controllers/supertrend.py
deleted file mode 100644
index 6da96c4..0000000
--- a/quants_lab/controllers/supertrend.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import time
-
-import pandas as pd
-from pydantic import Field
-
-from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
-from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
-from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_controller_base import (
- DirectionalTradingControllerBase,
- DirectionalTradingControllerConfigBase,
-)
-
-
-class SuperTrendConfig(DirectionalTradingControllerConfigBase):
- strategy_name: str = "supertrend"
- length: int = Field(default=20, ge=5, le=200)
- multiplier: float = Field(default=4.0, ge=2.0, le=7.0)
- percentage_threshold: float = Field(default=0.01, ge=0.005, le=0.05)
-
-
-class SuperTrend(DirectionalTradingControllerBase):
- def __init__(self, config: SuperTrendConfig):
- 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.supertrend(length=self.config.length, multiplier=self.config.multiplier, append=True)
- df["percentage_distance"] = abs(df["close"] - df[f"SUPERT_{self.config.length}_{self.config.multiplier}"]) / df["close"]
-
- # Generate long and short conditions
- long_condition = (df[f"SUPERTd_{self.config.length}_{self.config.multiplier}"] == 1) & (df["percentage_distance"] < self.config.percentage_threshold)
- short_condition = (df[f"SUPERTd_{self.config.length}_{self.config.multiplier}"] == -1) & (df["percentage_distance"] < self.config.percentage_threshold)
-
- # Choose side
- df['signal'] = 0
- df.loc[long_condition, 'signal'] = 1
- df.loc[short_condition, 'signal'] = -1
- return df
diff --git a/quants_lab/controllers/supertrend_multitimeframe.py b/quants_lab/controllers/supertrend_multitimeframe.py
deleted file mode 100644
index 2e389d3..0000000
--- a/quants_lab/controllers/supertrend_multitimeframe.py
+++ /dev/null
@@ -1,89 +0,0 @@
-import time
-from typing import Optional, Callable
-
-import pandas as pd
-from pydantic import Field
-
-from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
-from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
-from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_controller_base import (
- DirectionalTradingControllerBase,
- DirectionalTradingControllerConfigBase,
-)
-
-
-class SuperTrendMTConfig(DirectionalTradingControllerConfigBase):
- strategy_name: str = "supertrend_multitimeframe"
- length: int = Field(default=20, ge=5, le=200)
- multiplier: float = Field(default=4.0, ge=2.0, le=7.0)
- percentage_threshold: float = Field(default=0.01, ge=0.005, le=0.05)
-
-
-class SuperTrendMT(DirectionalTradingControllerBase):
- def __init__(self, config: SuperTrendMTConfig):
- 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
-
- @staticmethod
- def get_minutes_from_interval(interval: str):
- unit = interval[-1]
- quantity = int(interval[:-1])
- conversion = {"m": 1, "h": 60, "d": 1440}
- return conversion[unit] * quantity
-
- def ordered_market_data_dfs(self):
- market_data = {f"{candles.name}_{candles.interval}": candles.candles_df for candles in self.candles}
- return sorted(market_data.items(), key=lambda x: self.get_minutes_from_interval(x[0].split("_")[-1]))
-
- def get_dataframes_merged_by_min_resolution(self, add_indicators_func: Optional[Callable] = None):
- ordered_data = self.ordered_market_data_dfs()
- if add_indicators_func:
- processed_data = []
- for interval, df in ordered_data:
- processed_df = add_indicators_func(df)
- processed_data.append((interval, processed_df))
- else:
- processed_data = ordered_data
- interval_suffixes = {key: f'_{key.split("_")[-1]}' for key, _ in processed_data}
- merged_df = None
- for interval, df in processed_data:
- if merged_df is None:
- merged_df = df.copy()
- else:
- merged_df = pd.merge_asof(merged_df, df.add_suffix(interval_suffixes[interval]),
- left_on=f"timestamp", right_on=f"timestamp{interval_suffixes[interval]}",
- direction="backward")
- return merged_df
-
- def add_indicators(self, df):
- df.ta.supertrend(length=self.config.length, multiplier=self.config.multiplier, append=True)
- return df
-
- def get_processed_data(self) -> pd.DataFrame:
- df = self.get_dataframes_merged_by_min_resolution(self.add_indicators)
- df["percentage_distance"] = abs(df["close"] - df[f"SUPERT_{self.config.length}_{self.config.multiplier}"]) / df["close"]
-
- columns_with_supertrend = [col for col in df.columns if "SUPERTd" in col]
-
- # Conditions for long and short signals
- long_condition = df[columns_with_supertrend].apply(lambda x: all(item == 1 for item in x), axis=1)
- short_condition = df[columns_with_supertrend].apply(lambda x: all(item == -1 for item in x), axis=1)
-
- # Choose side
- df['signal'] = 0
- df.loc[long_condition, 'signal'] = 1
- df.loc[short_condition, 'signal'] = -1
- return df
diff --git a/quants_lab/controllers/trend_follower_v1.py b/quants_lab/controllers/trend_follower_v1.py
deleted file mode 100644
index 167de8f..0000000
--- a/quants_lab/controllers/trend_follower_v1.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import time
-from typing import Optional
-
-import pandas as pd
-from pydantic import Field
-
-from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
-from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
-from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_controller_base import (
- DirectionalTradingControllerBase,
- DirectionalTradingControllerConfigBase,
-)
-
-
-class TrendFollowerV1Config(DirectionalTradingControllerConfigBase):
- strategy_name: str = "trend_follower_v1"
- sma_fast: int = Field(default=20, ge=10, le=150)
- sma_slow: int = Field(default=100, ge=50, le=400)
- bb_length: int = Field(default=100, ge=100, le=200)
- bb_std: float = Field(default=2.0, ge=2.0, le=3.0)
- bb_threshold: float = Field(default=0.2, ge=0.1, le=0.5)
-
-
-class TrendFollowerV1(DirectionalTradingControllerBase):
-
- def __init__(self, config: TrendFollowerV1Config):
- 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_fast, append=True)
- df.ta.sma(length=self.config.sma_slow, append=True)
- df.ta.bbands(length=self.config.bb_length, std=2.0, append=True)
-
-
- # Generate long and short conditions
- bbp = df[f"BBP_{self.config.bb_length}_2.0"]
- inside_bounds_condition = (bbp < 0.5 + self.config.bb_threshold) & (bbp > 0.5 - self.config.bb_threshold)
-
- long_cond = (df[f'SMA_{self.config.sma_fast}'] > df[f'SMA_{self.config.sma_slow}'])
- short_cond = (df[f'SMA_{self.config.sma_fast}'] < df[f'SMA_{self.config.sma_slow}'])
-
- # Choose side
- df['signal'] = 0
- df.loc[long_cond & inside_bounds_condition, 'signal'] = 1
- df.loc[short_cond & inside_bounds_condition, 'signal'] = -1
- return df
-
- def extra_columns_to_show(self):
- return [f"BBP_{self.config.bb_length}_{self.config.bb_std}",
- f"SMA_{self.config.sma_fast}",
- f"SMA_{self.config.sma_slow}"]
diff --git a/quants_lab/research_notebooks/01_analyze_optimization_results.ipynb b/quants_lab/research_notebooks/analyze_optimization_results.ipynb
similarity index 100%
rename from quants_lab/research_notebooks/01_analyze_optimization_results.ipynb
rename to quants_lab/research_notebooks/analyze_optimization_results.ipynb
diff --git a/quants_lab/research_notebooks/dman_maker/01_strategy_design_dman_maker.ipynb b/quants_lab/research_notebooks/dman_maker/01_strategy_design_dman_maker.ipynb
new file mode 100644
index 0000000..5de4890
--- /dev/null
+++ b/quants_lab/research_notebooks/dman_maker/01_strategy_design_dman_maker.ipynb
@@ -0,0 +1,201 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": [
+ "# RESEARCH NOTEBOOK --> DMAN Maker V2"
+ ],
+ "metadata": {
+ "collapsed": false
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "100%|ββββββββββ| 10080/10080 [00:13<00:00, 772.37it/s]\n",
+ "Unclosed client session\n",
+ "client_session: \n",
+ "Unclosed connector\n",
+ "connections: ['[(, 127427.978703458)]']\n",
+ "connector: \n"
+ ]
+ }
+ ],
+ "source": [
+ "from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig\n",
+ "import asyncio\n",
+ "from utils.hummingbot_processes import aget_candles_list\n",
+ "\n",
+ "exchange = \"binance_perpetual\"\n",
+ "symbols = [\"XRP-USDT\"]\n",
+ "intervals = [\"3m\"]\n",
+ "max_records = 10080\n",
+ "\n",
+ "candles_config = [CandlesConfig(connector=exchange,\n",
+ " trading_pair=symbol,\n",
+ " interval=interval,\n",
+ " max_records=max_records) for symbol in symbols for interval in intervals]\n",
+ "\n",
+ "all_candles = await aget_candles_list(candles_config)"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-04-18T22:02:52.165106Z",
+ "start_time": "2024-04-18T22:02:39.073506Z"
+ }
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "\n",
+ "def analyze_candle_ranges(df, window_length):\n",
+ " # List to store range data\n",
+ " range_data = []\n",
+ "\n",
+ " # Loop through the DataFrame based on the window length\n",
+ " for start_idx in range(len(df) - window_length + 1):\n",
+ " # Define the window\n",
+ " end_idx = start_idx + window_length\n",
+ " window = df[start_idx:end_idx]\n",
+ "\n",
+ " # Find the max and min values within the window\n",
+ " max_price = window['high'].max()\n",
+ " min_price = window['low'].min()\n",
+ "\n",
+ " # Calculate the range\n",
+ " price_range = max_price - min_price\n",
+ " price_range_pct = price_range / min_price\n",
+ "\n",
+ " # Store the data\n",
+ " range_data.append((start_idx, start_idx + window_length, max_price, min_price, price_range, price_range_pct))\n",
+ "\n",
+ " # Create a DataFrame from the range data\n",
+ " columns = ['Start_Index', 'End_Index', 'Max_Price', 'Min_Price', 'Price_Range', 'Price_Range_Pct']\n",
+ " ranges_df = pd.DataFrame(range_data, columns=columns)\n",
+ "\n",
+ " return ranges_df\n",
+ "\n",
+ "# To use the function:\n",
+ "# Assuming 'df' is your DataFrame with 'high' and 'low' price columns\n"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-04-18T22:02:54.638744Z",
+ "start_time": "2024-04-18T22:02:54.635152Z"
+ }
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "outputs": [],
+ "source": [
+ "window_length = 240 # Replace with your desired\n",
+ "\n",
+ "range_analysis_df = analyze_candle_ranges(all_candles[0], window_length)"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-04-18T22:02:56.718508Z",
+ "start_time": "2024-04-18T22:02:56.321691Z"
+ }
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "outputs": [
+ {
+ "data": {
+ "text/plain": "",
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAA1sAAAIjCAYAAAD1OgEdAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAABPnklEQVR4nO3dfXyP9f////trvHZiNjMnO3Eyc5KTnBXRclrGnCSkIiujRR9tFdKJdxEqSjnPO/V+v6MTSjoRisxJVOS0IZWzZBXbRMwm87Id3z/89vr1spnt5XXstZfdrpeLS47jeB7H8TheD8e4d5y8LIZhGAIAAAAAuJSXuwsAAAAAgGsRYQsAAAAATEDYAgAAAAATELYAAAAAwASELQAAAAAwAWELAAAAAExA2AIAAAAAExC2AAAAAMAEhC0AAAAAMAFhCwBKqQkTJshisZTIvjp37qzOnTvbp7/66itZLBZ99NFHJbL/IUOGqE6dOiWyL2dlZmbqwQcfVGhoqCwWi0aOHGnavn799VdZLBYtWLDAtH0AAMxH2AKAErBgwQJZLBb7L19fX4WHhysmJkazZ8/WmTNnXLKfo0ePasKECUpOTnbJ9lypNNdWFJMnT9aCBQs0YsQIvfvuu7r//vsvO7ZOnToO/a5evbo6dOigTz/9tAQrLr5L6/b391ebNm30zjvvuLs0APBIFsMwDHcXAQDXugULFmjo0KGaNGmSIiMjZbPZlJqaqq+++kpJSUmqXbu2li1bpubNm9vXuXDhgi5cuCBfX98i72f79u266aabNH/+fA0ZMqTI650/f16S5O3tLenila1bb71VS5Ys0V133VXk7Thbm81mU25urnx8fFyyLzPcfPPNKl++vL755psrjq1Tp44qV66sxx9/XNLFoPnGG2/ol19+0euvv67/+7//K3R9wzCUnZ0tq9WqcuXKuaT+ori07mPHjum///2v9u/frzfffFPDhg0rsVoA4FpQ3t0FAEBZ0qNHD7Vu3do+PXbsWK1bt06333677rjjDv3000/y8/OTJJUvX17ly5v7Y/rs2bOqUKGCPWS5i9Vqdev+iyI9PV1NmjQp8vgaNWrovvvus08PHjxY9evX14wZMy4bti5cuKDc3Fx5e3sXK2S70qV1DxkyRHXr1tWMGTMIWwBQTNxGCABudtttt2ncuHE6cuSI3nvvPfv8gp7ZSkpKUvv27RUUFKSKFSuqYcOG+te//iXp4tWom266SZI0dOhQ+61gec/9dO7cWU2bNtWOHTvUsWNHVahQwb7upc9s5cnJydG//vUvhYaGyt/fX3fccYd+++03hzF16tQp8CraP7d5pdoKemYrKytLjz/+uGrVqiUfHx81bNhQr776qi69IcNisSgxMVFLly5V06ZN5ePjo+uvv16rVq0q+AO/RHp6uuLj4xUSEiJfX1+1aNFCb7/9tn153vNrhw8f1ueff26v/ddffy3S9vOEhoaqcePGOnz4sKT//7msV199VTNnzlS9evXk4+OjH3/88bLPbP3888+65557VK1aNfn5+alhw4Z65plnHMb88ccfeuCBBxQSEmL/LN56661i1fpP1apVU6NGjXTo0CGH+V9//bXuvvtu1a5dWz4+PqpVq5ZGjRqlv//+22HckCFDVLFiRf3xxx/q27evKlasqGrVqmnMmDHKyclxGHvixAndf//9CgwMVFBQkOLi4rRr167LfhZ33XWXgoOD5evrq9atW2vZsmUOY2w2myZOnKgGDRrI19dXVapUUfv27ZWUlOT05wEAxcGVLQAoBe6//37961//0urVqy979WDv3r26/fbb1bx5c02aNEk+Pj46ePCgvv32W0lS48aNNWnSJI0fP17Dhw9Xhw4dJEm33HKLfRsnTpxQjx49NHDgQN13330KCQkptK4XX3xRFotFTz31lNLT0zVz5kxFR0crOTnZfgWuKIpS2z8ZhqE77rhD69evV3x8vFq2bKkvv/xSTzzxhP744w/NmDHDYfw333yjTz75RA8//LACAgI0e/Zs9e/fXykpKapSpcpl6/r777/VuXNnHTx4UImJiYqMjNSSJUs0ZMgQnTp1So899pgaN26sd999V6NGjVLNmjXtt9hVq1atyMcvXfyH/2+//Zavnvnz5+vcuXMaPny4fHx8FBwcrNzc3Hzr7969Wx06dJDVatXw4cNVp04dHTp0SMuXL9eLL74oSUpLS9PNN99sD6DVqlXTypUrFR8fr4yMDKde6nHhwgX9/vvvqly5ssP8JUuW6OzZsxoxYoSqVKmirVu3as6cOfr999+1ZMkSh7E5OTmKiYlR27Zt9eqrr2rNmjWaNm2a6tWrpxEjRkiScnNz1bt3b23dulUjRoxQo0aN9NlnnykuLi5fTXv37lW7du1Uo0YNPf300/L399eHH36ovn376uOPP1a/fv0kXfwfFlOmTNGDDz6oNm3aKCMjQ9u3b9fOnTvVtWvXYn8WAFBsBgDAdPPnzzckGdu2bbvsmEqVKhk33HCDffq5554z/vljesaMGYYk4/jx45fdxrZt2wxJxvz58/Mt69SpkyHJmDdvXoHLOnXqZJ9ev369IcmoUaOGkZGRYZ//4YcfGpKMWbNm2edFREQYcXFxV9xmYbXFxcUZERER9umlS5cakowXXnjBYdxdd91lWCwW4+DBg/Z5kgxvb2+Hebt27TIkGXPmzMm3r3+aOXOmIcl477337PPOnz9vREVFGRUrVnQ49oiICKNXr16Fbu+fY7t162YcP37cOH78uLFr1y5j4MCBhiTjkUceMQzDMA4fPmxIMgIDA4309HSH9fOW/fOz6tixoxEQEGAcOXLEYWxubq799/Hx8UZYWJjx559/OowZOHCgUalSJePs2bPFqnvPnj3G/fffb0gyEhISHMYWtK0pU6YYFovFoca4uDhDkjFp0iSHsTfccIPRqlUr+/THH39sSDJmzpxpn5eTk2Pcdttt+T6LLl26GM2aNTPOnTvn8DnccsstRoMGDezzWrRoUeSeAYAZuI0QAEqJihUrFvpWwqCgIEnSZ599VuCVj6Lw8fHR0KFDizx+8ODBCggIsE/fddddCgsL0xdffOHU/ovqiy++ULly5fToo486zH/88cdlGIZWrlzpMD86Olr16tWzTzdv3lyBgYH65Zdfrrif0NBQ3XvvvfZ5VqtVjz76qDIzM7Vhwwanj2H16tWqVq2aqlWrphYtWmjJkiW6//779fLLLzuM69+//xWvkh0/flwbN27UAw88oNq1azssy7vV1DAMffzxx+rdu7cMw9Cff/5p/xUTE6PTp09r586dxaq7WbNmevfddzV06FC98sorDuP+eWUzKytLf/75p2655RYZhqHvv/8+33YvfU6tQ4cODv1ZtWqVrFarw5VdLy8vJSQkOKx38uRJrVu3Tvfcc4/OnDljP8YTJ04oJiZGBw4c0B9//CHp4jmzd+9eHThw4IrHDQBmIGwBQCmRmZnpEGwuNWDAALVr104PPvigQkJCNHDgQH344YfFCl41atQo1sswGjRo4DBtsVhUv379Yj+vVFxHjhxReHh4vs+jcePG9uX/dGkAkaTKlSvrr7/+uuJ+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HHXr1k3bt29XamqqKW/TBAAz8cwWAAAoFR555BH9/fffioqKUnZ2tj755BNt2rRJkydPJmgB8Ehc2QIAAKXCokWLNG3aNB08eFDnzp1T/fr1NWLECF5oAcBjEbYAAAAAwAR8zxYAAAAAmICwBQAAAAAm4AUZRZCbm6ujR48qICDA4UsVAQAAAJQthmHozJkzCg8Pl5dX4deuCFtFcPToUdWqVcvdZQAAAAAoJX777TfVrFmz0DGErSIICAiQdPEDDQwMdMk2bTabVq9erW7duslqtbpkm3A9+uQ56JVnoE+eg155BvrkOeiVZyhKnzIyMlSrVi17RigMYasI8m4dDAwMdGnYqlChggIDAznhSjH65DnolWegT56DXnkG+uQ56JVnKE6fivJ4ES/IAAAAAAATELYAAAAAwASELQAAAAAwAWELAAAAAExA2AIAAAAAExC2AAAAAMAEhC0AAAAAMAFhCwAAAABMQNgCAAAAABMQtgAAAADABIQtAAAAADABYQsAAAAATEDYAgAAAAATELYAAAAAwASELQAAAAAwAWELAAAAAExA2AIAAAAAExC2AAAAAMAEhC0AAAAAMEF5dxeAktW7t3PrLV/u2joAAACAax1XtgAAAADABIQtAAAAADABYQsAAAAATEDYAgAAAAATELYAAAAAwASELQAAAAAwAWELAAAAAEzg1rC1ceNG9e7dW+Hh4bJYLFq6dKl9mc1m01NPPaVmzZrJ399f4eHhGjx4sI4ePeqwjZMnTyo2NlaBgYEKCgpSfHy8MjMzHcbs3r1bHTp0kK+vr2rVqqWpU6eWxOEBAAAAKMPcGraysrLUokULzZ07N9+ys2fPaufOnRo3bpx27typTz75RPv27dMdd9zhMC42NlZ79+5VUlKSVqxYoY0bN2r48OH25RkZGerWrZsiIiK0Y8cOvfLKK5owYYLefPNN048PAAAAQNlV3p0779Gjh3r06FHgskqVKikpKclh3muvvaY2bdooJSVFtWvX1k8//aRVq1Zp27Ztat26tSRpzpw56tmzp1599VWFh4dr4cKFOn/+vN566y15e3vr+uuvV3JysqZPn+4QygAAAADAldwatorr9OnTslgsCgoKkiRt3rxZQUFB9qAlSdHR0fLy8tKWLVvUr18/bd68WR07dpS3t7d9TExMjF5++WX99ddfqly5cr79ZGdnKzs72z6dkZEh6eKtjTabzSXHkrcdV22vqKxW59Yr4TJLDXf1CcVHrzwDffIc9Moz0CfPQa88Q1H6VJweekzYOnfunJ566inde++9CgwMlCSlpqaqevXqDuPKly+v4OBgpaam2sdERkY6jAkJCbEvKyhsTZkyRRMnTsw3f/Xq1apQoYJLjifPpVfvzBYX59x6X3zh2jo8TUn3Cc6jV56BPnkOeuUZ6JPnoFeeobA+nT17tsjb8YiwZbPZdM8998gwDL3++uum72/s2LEaPXq0fTojI0O1atVSt27d7EHvatlsNiUlJalr166yOnu5yQkDBji33uLFrq3DU7irTyg+euUZ6JPnoFeegT55DnrlGYrSp7y73oqi1IetvKB15MgRrVu3ziHshIaGKj093WH8hQsXdPLkSYWGhtrHpKWlOYzJm84bcykfHx/5+Pjkm2+1Wl1+cpixzcI4e+W6rP9MKOk+wXn0yjPQJ89BrzwDffIc9MozFNan4vSvVH/PVl7QOnDggNasWaMqVao4LI+KitKpU6e0Y8cO+7x169YpNzdXbdu2tY/ZuHGjw72VSUlJatiwYYG3EAIAAACAK7g1bGVmZio5OVnJycmSpMOHDys5OVkpKSmy2Wy66667tH37di1cuFA5OTlKTU1Vamqqzp8/L0lq3LixunfvrmHDhmnr1q369ttvlZiYqIEDByo8PFySNGjQIHl7eys+Pl579+7V4sWLNWvWLIfbBAEAAADA1dx6G+H27dt166232qfzAlBcXJwmTJigZcuWSZJatmzpsN769evVuXNnSdLChQuVmJioLl26yMvLS/3799fs2bPtYytVqqTVq1crISFBrVq1UtWqVTV+/Hhe+w4AAADAVG4NW507d5ZhGJddXtiyPMHBwVq0aFGhY5o3b66vv/662PUBAAAAgLNK9TNbAAAAAOCpCFsAAAAAYALCFgAAAACYgLAFAAAAACYgbAEAAACACQhbAAAAAGACwhYAAAAAmICwBQAAAAAmIGwBAAAAgAkIWwAAAABgAsIWAAAAAJiAsAUAAAAAJijv7gJwbevd27n1li93bR0AAABASePKFgAAAACYgLAFAAAAACYgbAEAAACACQhbAAAAAGACwhYAAAAAmICwBQAAAAAmIGwBAAAAgAkIWwAAAABgAsIWAAAAAJigvLsLAFypd2/n1lu+3LV1AAAAAIQtlErOhiYAAACgtOA2QgAAAAAwAWELAAAAAExA2AIAAAAAExC2AAAAAMAEhC0AAAAAMAFhCwAAAABMQNgCAAAAABMQtgAAAADABIQtAAAAADABYQsAAAAATEDYAgAAAAATELYAAAAAwASELQAAAAAwAWELAAAAAExA2AIAAAAAExC2AAAAAMAEhC0AAAAAMAFhCwAAAABMQNgCAAAAABMQtgAAAADABIQtAAAAADABYQsAAAAATEDYAgAAAAATELYAAAAAwASELQAAAAAwAWELAAAAAExA2AIAAAAAExC2AAAAAMAEhC0AAAAAMAFhCwAAAABMQNgCAAAAABMQtgAAAADABIQtAAAAADABYQsAAAAATEDYAgAAAAATELYAAAAAwARuDVsbN25U7969FR4eLovFoqVLlzosNwxD48ePV1hYmPz8/BQdHa0DBw44jDl58qRiY2MVGBiooKAgxcfHKzMz02HM7t271aFDB/n6+qpWrVqaOnWq2YcGAAAAoIxza9jKyspSixYtNHfu3AKXT506VbNnz9a8efO0ZcsW+fv7KyYmRufOnbOPiY2N1d69e5WUlKQVK1Zo48aNGj58uH15RkaGunXrpoiICO3YsUOvvPKKJkyYoDfffNP04wMAAABQdpV358579OihHj16FLjMMAzNnDlTzz77rPr06SNJeueddxQSEqKlS5dq4MCB+umnn7Rq1Spt27ZNrVu3liTNmTNHPXv21Kuvvqrw8HAtXLhQ58+f11tvvSVvb29df/31Sk5O1vTp0x1CGQAAAAC4klvDVmEOHz6s1NRURUdH2+dVqlRJbdu21ebNmzVw4EBt3rxZQUFB9qAlSdHR0fLy8tKWLVvUr18/bd68WR07dpS3t7d9TExMjF5++WX99ddfqly5cr59Z2dnKzs72z6dkZEhSbLZbLLZbC45vrztuGp7RWW1Orees2U6u7+Sdrnjc1efUHz0yjPQJ89BrzwDffIc9MozFKVPxelhqQ1bqampkqSQkBCH+SEhIfZlqampql69usPy8uXLKzg42GFMZGRkvm3kLSsobE2ZMkUTJ07MN3/16tWqUKGCk0dUsKSkJJdu70ri4pxb74svSnZ/Je1Kx1fSfYLz6JVnoE+eg155BvrkOeiVZyisT2fPni3ydkpt2HKnsWPHavTo0fbpjIwM1apVS926dVNg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MDVp0sRhXuPGjfXxxx9LkkJDQyVJaWlpDle20tLS1LJlS/uY9PR0h21cuHBBJ0+etK9/KR8fH/n4+OSbb7VaXX5ymLHNwnDl2jk2m7XAsFXStwPys/nKSvqcgnPok+egV56BPnkOeuUZCutTcfrn1NsIR40aJavVqpSUFIfb6gYMGKBVq1Y5s8kCtWvXLt/tf/v371dERISkiy/LCA0N1dq1a+3LMzIytGXLFkVFRUmSoqKidOrUKe3YscM+Zt26dcrNzVXbtm1dVisAAAAA/JNTV7ZWr16tL7/8UjVr1nSY36BBAx05csQlhUkXQ90tt9yiyZMn65577tHWrVv15ptv6s0335QkWSwWjRw5Ui+88IIaNGigyMhIjRs3TuHh4erbt6+ki1fCunfvrmHDhmnevHmy2WxKTEzUwIEDPfpNhL17u7sCAAAAAIVxKmxlZWUV+KKIkydPFnj7nbNuuukmffrppxo7dqwmTZqkyMhIzZw5U7GxsfYxTz75pLKysjR8+HCdOnVK7du316pVq+wv15CkhQsXKjExUV26dJGXl5f69++v2bNnu6xOAAAAALiUU2GrQ4cOeuedd/T8889LuniFKTc3V1OnTtWtt97q0gJvv/123X777ZddbrFYNGnSJE2aNOmyY4KDg7Vo0SKX1gUAAAAAhXEqbE2dOlVdunTR9u3bdf78eT355JPau3evTp48qW+//dbVNQIAAACAx3HqBRlNmzbV/v371b59e/Xp00dZWVm688479f3336tevXqurhEAAAAAPE6xr2zZbDZ1795d8+bN0zPPPGNGTQAAAADg8Yp9ZctqtWr37t1m1AIAAAAA1wynbiO877779L///c/VtQAAAADANcOpF2RcuHBBb731ltasWaNWrVrJ39/fYfn06dNdUhwAAAAAeKpiha1ffvlFderU0Q8//KAbb7xRkrR//36HMRaLxXXVAQAAAICHKlbYatCggY4dO6b169dLkgYMGKDZs2crJCTElOIAAAAAwFMV65ktwzAcpleuXKmsrCyXFgQAAAAA1wKnXpCR59LwBQAAAAC4qFhhy2Kx5Hsmi2e0AAAAACC/Yj2zZRiGhgwZIh8fH0nSuXPn9H//93/53kb4ySefuK5CAHa9ezu/7vLlrqsDAAAAV1assBUXF+cwfd9997m0GAAAAAC4VhQrbM2fP9+sOgAAAADgmnJVL8gAAAAAABSMsAUAAAAAJiBsAQAAAIAJCFsAAAAAYALCFgAAAACYgLAFAAAAACYgbAEAAACACYr1PVsAHPXu7e4KAAAAUFpxZQsAAAAATEDYAgAAAAATELYAAAAAwASELQAAAAAwAWELAAAAAExA2AIAAAAAExC2AAAAAMAEhC0AAAAAMAFhCwAAAABMQNgCAAAAABMQtgAAAADABIQtAAAAADABYQsAAAAATEDYAgAAAAATELYAAAAAwASELQAAAAAwAWELAAAAAExA2AIAAAAAExC2AAAAAMAEhC0AAAAAMAFhCwAAAABMQNgCAAAAABMQtgAAAADABIQtAAAAADABYQsAAAAATEDYAgAAAAATELYAAAAAwASELQAAAAAwAWELAAAAAExA2AIAAAAAExC2AAAAAMAEhC0AAAAAMAFhCwAAAABMQNgCAAAAABMQtgAAAADABIQtAAAAADABYQsAAAAATEDYAgAAAAATeFTYeumll2SxWDRy5Ej7vHPnzikhIUFVqlRRxYoV1b9/f6WlpTmsl5KSol69eqlChQqqXr26nnjiCV24cKGEqwcAAABQlnhM2Nq2bZveeOMNNW/e3GH+qFGjtHz5ci1ZskQbNmzQ0aNHdeedd9qX5+TkqFevXjp//rw2bdqkt99+WwsWLND48eNL+hAAAAAAlCEeEbYyMzMVGxur//znP6pcubJ9/unTp/W///1P06dP12233aZWrVpp/vz52rRpk7777jtJ0urVq/Xjjz/qvffeU8uWLdWjRw89//zzmjt3rs6fP++uQwIAAABwjSvv7gKKIiEhQb169VJ0dLReeOEF+/wdO3bIZrMpOjraPq9Ro0aqXbu2Nm/erJtvvlmbN29Ws2bNFBISYh8TExOjESNGaO/evbrhhhvy7S87O1vZ2dn26YyMDEmSzWaTzWZzyTHlbcfZ7VmtLikDV2C12hz+68lc9Ee31Lracwolgz55DnrlGeiT56BXnqEofSpOD0t92Prggw+0c+dObdu2Ld+y1NRUeXt7KygoyGF+SEiIUlNT7WP+GbTyluctK8iUKVM0ceLEfPNXr16tChUqOHMYl5WUlOTUenFxLi0DVzBokHN9Kk2++MLdFZQMZ88plCz65DnolWegT56DXnmGwvp09uzZIm+nVIet3377TY899piSkpLk6+tbYvsdO3asRo8ebZ/OyMhQrVq11K1bNwUGBrpkHzabTUlJSeratausTlymGjDAJWXgCqxWmwYNStKiRV1ls3n25cTFi91dgbmu9pxCyaBPnoNeeQb65DnolWcoSp/y7norilIdtnbs2KH09HTdeOON9nk5OTnauHGjXnvtNX355Zc6f/68Tp065XB1Ky0tTaGhoZKk0NBQbd261WG7eW8rzBtzKR8fH/n4+OSbb7VaXX5yOLtNrkCXLJvN6vFhq6z8XDfjPIXr0SfPQa88A33yHPTKMxTWp+L0r1S/IKNLly7as2ePkpOT7b9at26t2NhY+++tVqvWrl1rX2ffvn1KSUlRVFSUJCkqKkp79uxRenq6fUxSUpICAwPVpEmTEj8mAAAAAGVDqb6yFRAQoKZNmzrM8/f3V5UqVezz4+PjNXr0aAUHByswMFCPPPKIoqKidPPNN0uSunXrpiZNmuj+++/X1KlTlZqaqmeffVYJCQkFXr0CAAAAAFco1WGrKGbMmCEvLy/1799f2dnZiomJ0b///W/78nLlymnFihUaMWKEoqKi5O/vr7i4OE2aNMmNVQMAAAC41nlc2Prqq68cpn19fTV37lzNnTv3sutEREToi7LyKjYAAAAApUKpfmYLAAAAADwVYQsAAAAATEDYAgAAAAATELYAAAAAwASELQAAAAAwAWELAAAAAExA2AIAAAAAE3jc92wBcE7v3s6tt3y5a+sAAAAoK7iyBQAAAAAmIGwBAAAAgAkIWwAAAABgAsIWAAAAAJiAsAUAAAAAJiBsAQAAAIAJCFsAAAAAYALCFgAAAACYgLAFAAAAACYgbAEAAACACQhbAAAAAGACwhYAAAAAmICwBQAAAAAmIGwBAAAAgAkIWwAAAABgAsIWAAAAAJiAsAUAAAAAJiBsAQAAAIAJCFsAAAAAYALCFgAAAACYgLAFAAAAACYgbAEAAACACQhbAAAAAGACwhYAAAAAmICwBQAAAAAmIGwBAAAAgAkIWwAAAABgAsIWAAAAAJiAsAUAAAAAJiBsAQAAAIAJCFsAAAAAYALCFgAAAACYgLAFAAAAACYgbAEAAACACQhbAAAAAGACwhYAAAAAmICwBQAAAAAmIGwBAAAAgAkIWwAAAABgAsIWAAAAAJiAsAUAAAAAJiBsAQAAAIAJCFsAAAAAYALCFgAAAACYgLAFAAAAACYgbAEAAACACQhbAAAAAGACwhYAAAAAmICwBQAAAAAmIGwBAAAAgAkIWwAAAABgAsIWAAAAAJiAsAUAAAAAJijVYWvKlCm66aabFBAQoOrVq6tv377at2+fw5hz584pISFBVapUUcWKFdW/f3+lpaU5jElJSVGvXr1UoUIFVa9eXU888YQuXLhQkocCAAAAoIwp1WFrw4YNSkhI0HfffaekpCTZbDZ169ZNWVlZ9jGjRo3S8uXLtWTJEm3YsEFHjx7VnXfeaV+ek5OjXr166fz589q0aZPefvttLViwQOPHj3fHIQEAAAAoI8q7u4DCrFq1ymF6wYIFql69unbs2KGOHTvq9OnT+t///qdFixbptttukyTNnz9fjRs31nfffaebb75Zq1ev1o8//qg1a9YoJCRELVu21PPPP6+nnnpKEyZMkLe3d779ZmdnKzs72z6dkZEhSbLZbLLZbC45trztOLs9q9UlZeAKrFabw3/LIhf9kTfd1Z5TKBn0yXPQK89AnzwHvfIMRelTcXpoMQzDuOqqSsjBgwfVoEED7dmzR02bNtW6devUpUsX/fXXXwoKCrKPi4iI0MiRIzVq1CiNHz9ey5YtU3Jysn354cOHVbduXe3cuVM33HBDvv1MmDBBEydOzDd/0aJFqlChghmHBgAAAMADnD17VoMGDdLp06cVGBhY6NhSfWXrn3JzczVy5Ei1a9dOTZs2lSSlpqbK29vbIWhJUkhIiFJTU+1jQkJC8i3PW1aQsWPHavTo0fbpjIwM1apVS926dbviB1pUNptNSUlJ6tq1q6xOXKYaMMAlZeAKrFabBg1K0qJFXWWzlc3LiYsXu7uCornacwolgz55DnrlGeiT56BXnqEofcq7660oPCZsJSQk6IcfftA333xj+r58fHzk4+OTb77VanX5yeHsNrkCXbJsNmuZDVue9veBGecpXI8+eQ565Rnok+egV56hsD4Vp3+l+gUZeRITE7VixQqtX79eNWvWtM8PDQ3V+fPnderUKYfxaWlpCg0NtY+59O2EedN5YwAAAADA1Up12DIMQ4mJifr000+1bt06RUZGOixv1aqVrFar1q5da5+3b98+paSkKCoqSpIUFRWlPXv2KD093T4mKSlJgYGBatKkSckcCAAAAIAyp1TfRpiQkKBFixbps88+U0BAgP0Zq0qVKsnPz0+VKlVSfHy8Ro8e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Uw899JAeffTRfMtq16592fW8vLxUv359SVLjxo116NAhjRgxQu+++64kafPmzYqNjdXEiRMVExOjSpUq6YMPPtC0adMctmO1Wh2mLRaLcnNzC6350vrLlSunHTt2ONzCKEkVK1Ys8nYAAOYibAEASh1/f397qHFG8+bN9fvvv2v//v0FXt268cYb9eOPP17VPiTp6aefVr169TRq1CjdeOON2rRpkyIiIvTMM8/Yxxw5cqRY26xbt66sVqu2bdtmD36nT5/W/v371bFjR0nSDTfcoJycHKWnp6tDhw5XdQwAAPPwggwAwDWnU6dO6tixo/r376+kpCQdPnxYK1eu1KpVqyRdfGPgpk2blJiYqOTkZB04cECfffZZsV6QIUm1atVSv379NH78eEkXn/1KSUnRBx98oEOHDmn27Nn69NNPi7XNgIAAxcXF6YknntD69eu1d+9excfHy8vLSxaLRZJ03XXXKTY2VoMHD9Ynn3yiw4cPa+vWrZoyZYo+//zzYu0PAGAewhYA4Jr08ccf66abbtK9996rJk2a6Mknn1ROTo6ki1e+NmzYoP3796tDhw664YYbNH78eIeXUBTVqFGj9Pnnn2vr1q264447NGrUKCUmJqply5batGmTxo0bV+xtTp8+XVFRUbr99tsVHR2tdu3aqXHjxg4v7pg/f74GDx6sxx9/XA0bNlTfvn0droYBANzPYhiG4e4iAADA5WVlZalGjRqaNm2a4uPj3V0OAKCIeGYLAIBS5vvvv9fPP/+sNm3a6PTp05o0aZIkqU+fPm6uDABQHIQtAABKoVdffVX79u2Tt7e3WrVqpa+//lpVq1Z1d1kAgGLgNkIAAAAAMAEvyAAAAAAAExC2AAAAAMAEhC0AAAAAMAFhCwAAAABMQNgCAAAAABMQtgAAAADABIQtAAAAADABYQsAAAAATPD/AKj1Ta49MNZKAAAAAElFTkSuQmCC"
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "# Assuming range_analysis_df is your DataFrame from the previous function and has a 'Price_Range' column\n",
+ "plt.figure(figsize=(10, 6))\n",
+ "plt.hist(range_analysis_df['Price_Range_Pct'] * 100, bins=50, alpha=0.7, color='blue')\n",
+ "plt.title('Distribution of Price Ranges')\n",
+ "plt.xlabel('Price Range')\n",
+ "plt.ylabel('Frequency')\n",
+ "plt.grid(True)\n",
+ "plt.show()\n"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-04-18T22:03:04.360281Z",
+ "start_time": "2024-04-18T22:03:04.258530Z"
+ }
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "outputs": [
+ {
+ "data": {
+ "text/plain": "",
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABH8AAAJwCAYAAAAQrc1tAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAC5IklEQVR4nOzdeXxU1f3/8fdkskBYgqwJJCiCS1WsiqCoUVDAoiI04F632tZWbUFUqv1al7oWFaE/16rVuuBGI7gLbogLgihVu1iIIIsEQUogBLJM5vfH7ZBtlnvvnDtbXs/HIw8yM2fuPQm5M3fe95zP8QWDwaAAAAAAAACQkbKS3QEAAAAAAAB4h/AHAAAAAAAggxH+AAAAAAAAZDDCHwAAAAAAgAxG+AMAAAAAAJDBCH8AAAAAAAAyGOEPAAAAAABABiP8AQAAAAAAyGCEPwAAAAAAABmM8AcAAER0wQUXaK+99kp2N5BmbrjhBvl8vmR3AwAA/A/hDwAAGeqxxx6Tz+fb/dWhQwftu+++uuyyy7Rx48Zkdy+i1v3Ozs5Wv379dMEFF2j9+vXJ7p5xH3zwgX784x+rT58+ysvL01577aWLL75Ya9asSXbXWthrr71a/L9E+nrssceS3VUAANCKLxgMBpPdCQAAYN5jjz2mCy+8UH/4wx80YMAA7dq1S++//76eeOIJ7bnnnvryyy+Vn58fdRv19fVqbGxUXl5egnodvt+LFy/WY489pr322ktffvmlOnTokLD+eOn//b//p8mTJ2vvvffWBRdcoKKiIv3rX//Sww8/LEl69dVXddRRRyW5l5a5c+equrp69+1XX31VTz/9tO6++2717Nlz9/1HHXWU+vfvr4aGhoz5fwIAIN1lJ7sDAADAW2PHjtXhhx8uSfrZz36mHj16aMaMGZo3b57OOuussM/ZsWOHOnXqpJycnER2tYXW/e7Zs6f++Mc/6sUXX9Tpp5+etH6Z8sEHH2jKlCk65phj9Prrr7cI4n71q1/p6KOP1qRJk/SPf/xDe+yxR8L6Ffq/b23ChAktbldWVurpp5/WhAkTwk4NzM7mNBMAgFTBtC8AANqZ448/XpK0atUqSVZdn86dO6uiokInnXSSunTponPOOWf3Y60/2Dc2NmrWrFkaPHiwOnTooF69eulHP/qRPvnkkxbtnnzySQ0ZMkQdO3ZU9+7ddeaZZ2rt2rWu+11aWipJqqio2H1fXV2drrvuOg0ZMkQFBQXq1KmTSktL9c4777R47urVq+Xz+XTnnXfqz3/+swYOHKi8vDwNHTpUS5cubbOv559/XgcccIA6dOiggw46SC+88ELE38XMmTN14IEHqkOHDurTp48uvvhi/fe//43589x0003y+Xz661//2mYE1sCBAzV9+nRt2LBBDz74oCTpzjvvlM/n0zfffNNmW9dcc41yc3Nb7Pfjjz/Wj370IxUUFCg/P1/HHXecPvjggxbPC9Xm+ec//6mzzz5be+yxh4455piYfY8lXM0fn8+nyy67bPfvtmPHjho+fLi++OILSdKDDz6oQYMGqUOHDhoxYoRWr17dZrt2fiYAANAW4Q8AAO1MKDzp0aPH7vsaGhp04oknqnfv3rrzzjs1ceLEiM+/6KKLNGXKFJWUlOiPf/yjrr76anXo0EGLFy/e3eaWW27Reeedp3322UczZszQlClT9NZbb+nYY4/V1q1bXfU7FAY0HwWzbds2PfzwwxoxYoT++Mc/6oYbbtCmTZt04oknavny5W22MXv2bN1xxx26+OKLdfPNN2v16tUqKytTfX397javvPKKzjjjDOXk5Oi2225TWVmZLrroIi1btqzN9i6++GJdddVVOvroozVr1ixdeOGFeuqpp3TiiSe22GZrNTU1euutt1RaWqoBAwaEbXPGGWcoLy9PL7/8siTp9NNPl8/n03PPPdem7XPPPacxY8bs/t28/fbbOvbYY7Vt2zZdf/31uvXWW7V161Ydf/zxWrJkSZvnn3baaaqpqdGtt96qn//85xH7Ha9Fixbpiiuu0Pnnn68bbrhB//rXv3TKKafo3nvv1Z/+9Cddcskluuqqq/TRRx/ppz/9aYvnOv2ZAABAM0EAAJCRHn300aCk4JtvvhnctGlTcO3atcFnnnkm2KNHj2DHjh2D69atCwaDweD5558flBS8+uqr22zj/PPPD+655567b7/99ttBScHf/OY3bdo2NjYGg8FgcPXq1UG/3x+85ZZbWjz+xRdfBLOzs9vcb6ffc+bMCfbq1SuYl5cXXLt27e62DQ0Nwdra2hbP/+9//xvs06dP8Kc//enu+1atWhWUFOzRo0dwy5Ytu++fN29eUFLwpZde2n3f4MGDg8XFxcHt27fvvu/dd98NSmrxu1i0aFFQUvCpp55qsf/XX3897P3NLV++PCgpOHny5Ki/i4MPPjjYvXv33beHDx8eHDJkSIs2S5YsCUoKPv7448Fg0Pp/2GeffYInnnji7v+TYDAYrKmpCQ4YMCA4evTo3fddf/31QUnBs846K2o/wrnjjjuCkoKrVq1q81hou81JCubl5bVo/+CDDwYlBQsLC4Pbtm3bff8111zTYttOfiYAANAWI38AAMhwo0aNUq9evVRSUqIzzzxTnTt31gsvvKB+/fq1aPerX/0q5rb+9re/yefz6frrr2/zWGiaT3l5uRobG3X66adr8+bNu78KCwu1zz77tJmSZaffkyZNUqdOnfTiiy+quLh4dxu/36/c3FxJ1hSsLVu2qKGhQYcffrg+/fTTNts844wzWowcCk0l+/rrryVJ3377rb744gudd9556ty58+52xx13nAYPHtxiW88//7wKCgo0evToFj/nkCFD1Llz56g/5/bt2yVJXbp0ifo76NKli7Zt29ai/8uWLWsx9e3ZZ59VXl6exo8fL0lavny5VqxYobPPPlvff//97n7t2LFDJ5xwgt577z01Nja22M8vf/nLqP0w5YQTTmgxde6II46QJE2cOLHF7yJ0f+j/xc3PBAAAmlCJDwCADHfvvfdq3333VXZ2tvr06aP99ttPWVktr/9kZ2e3CFUiqaioUN++fdW9e/eIbVasWKFgMKh99tkn7ON2i0iH+l1VVaW//OUveu+998KuOvbXv/5Vd911l/7973+3mGoVbjpV//79W9wOBUGhWjmhejqDBg1q89xBgwa1CJRWrFihqqoq9e7dO2z/v/vuu4g/WyjoCIVAkWzfvr1FKHLaaadp6tSpevbZZ/W73/1OwWBQzz//vMaOHauuXbvu7pcknX/++RG3W1VV1SIEizT1zLTWv/+CggJJUklJSdj7Q/8vbn4mAADQhPAHAIAMN2zYsN2rZkWSl5fXJhByq7GxUT6fT6+99pr8fn+bx5uPqImmeb8nTJigY445Rmeffba++uqr3dt48skndcEFF2jChAm66qqr1Lt3b/n9ft12220tRseEhOuPJAWDQbs/3m6NjY3q3bu3nnrqqbCP9+rVK+JzBw0apOzsbH3++ecR29TW1uqrr75q8X/Xt29flZaW6rnnntPvfvc7LV68WGvWrNEf//jHFv2SpDvuuEOHHHJI2G23/j/o2LFjxH6YFOn3H+v/xc3PBAAAmhD+AAAA2wYOHKg33nhDW7ZsiTj6Z+DAgQoGgxowYID23XdfI/sNBTojR47UPffco6uvvlqSNGfOHO29994qLy9vsbpUuGlpduy5556SpJUrV7Z5rPV9AwcO1Jtvvqmjjz7acXjSqVMnjRw5Um+//ba++eab3ftt7rnnnlNtba1OOeWUFvefccYZuuSSS/TVV1/p2WefVX5+vsaNG9eiX5LUtWtXjRo1ylG/UlUm/kwAACQSNX8AAIBtEydOVDAY1I033tjmsdAojbKyMvn9ft14441tRtQEg0F9//33rvY9YsQIDRs2TDNnztSuXbskNY0Yab6fjz/+WB999JGrffTt21cHHXSQHn/8cVVXV+++f+HChbuXJA85/fTTFQgEdNNNN7XZTkNDQ8xVza699loFg0FdcMEF2rlzZ4vHVq1apWnTpqmoqEgXX3xxi8cmTpwov9+vp59+Ws8//7xOOeUUderUaffjQ4YM0cCBA3XnnXe2+BlCNm3aFLVfqSgTfyYAABKJkT8AAMC2kSNH6txzz9Wf/vQnrVixQj/60Y/U2NioRYsWaeTIkbrssss0cOBA3Xzzzbrmmmu0evVqTZgwQV26dNGqVav0wgsv6Be/+IWuvPJKV/u/6qqrdNppp+mxxx7TL3/5S51yyikqLy/Xj3/8Y5188slatWqVHnjgAR1wwAFhQwI7br31Vo0fP15HH320LrzwQv33v//VPffco4MOOqjFNo877jhdfPHFuu2227R8+XKNGTNGOTk5WrFihZ5//nnNmjVLkyZNirifY489VnfeeaemTp2qgw8+WBdccIGKior073//Ww899JAaGxv16quvtqlj07t3b40cOVIzZszQ9u3bdcYZZ7R4PCsrSw8//LDGjh2rAw88UBdeeKH69eun9evX65133lHXrl310ksvufrdJEsm/kwAACQS4Q8AAHDk0Ucf1cEHH6xHHnlEV111lQoKCnT44YfrqKOO2t3m6quv1r777qu777579yihkpISjRkzRqeeeqrrfZeVle0eAfLzn/9cF1xwgSorK/Xggw/qjTfe0AEHHKAnn3xSzz//vN59911X+xg3bpyefvpp3XDDDbr66qu1zz776LHHHtNf//pX/eMf/2jR9oEHHtCQIUP04IMP6ne/+52ys7O111576Sc/+YmOPvromPu6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"
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plt.figure(figsize=(14, 7))\n",
+ "plt.plot(range_analysis_df['Start_Index'], range_analysis_df['Price_Range_Pct'] * 100, marker='o', linestyle='-', color='blue')\n",
+ "plt.title('Price Range Over Time')\n",
+ "plt.xlabel('Index')\n",
+ "plt.ylabel('Price Range')\n",
+ "plt.grid(True)\n",
+ "plt.show()\n"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-04-18T22:03:34.608164Z",
+ "start_time": "2024-04-18T22:03:34.504796Z"
+ }
+ }
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 2
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython2",
+ "version": "2.7.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/quants_lab/research_notebooks/dman_maker/02_single_controller_backtest.ipynb b/quants_lab/research_notebooks/dman_maker/02_single_controller_backtest.ipynb
new file mode 100644
index 0000000..b480200
--- /dev/null
+++ b/quants_lab/research_notebooks/dman_maker/02_single_controller_backtest.ipynb
@@ -0,0 +1,39262 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
+ "ExecuteTime": {
+ "end_time": "2024-05-21T23:33:43.257748Z",
+ "start_time": "2024-05-21T23:33:43.251873Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "import sys\n",
+ "\n",
+ "root_path = os.path.abspath(os.path.join(os.getcwd(), '../../..'))\n",
+ "sys.path.append(root_path)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 38,
+ "outputs": [],
+ "source": [
+ "from decimal import Decimal\n",
+ "\n",
+ "# Controller configuration\n",
+ "connector_name = \"kucoin\"\n",
+ "trading_pair = \"WLD-USDT\"\n",
+ "total_amount_quote = 1000\n",
+ "trade_cost = 0.0006\n",
+ "buy_spreads = [Decimal(\"0.002\")]\n",
+ "sell_spreads = [Decimal(\"0.002\")]\n",
+ "buy_amounts_pct = [Decimal(\"1\")]\n",
+ "sell_amounts_pct = [Decimal(\"1\")]\n",
+ "executor_refresh_time = 60\n",
+ "cooldown_time = 3600\n",
+ "top_executor_refresh_time = 60\n",
+ "executor_activation_bounds = [Decimal(\"0.01\")]\n",
+ "dca_spreads = [Decimal(\"0.\"), Decimal(\"0.002\"), Decimal(\"0.008\"), Decimal(\"0.01\"), Decimal(\"0.015\"), Decimal(\"0.02\")]\n",
+ "dca_amounts = [Decimal(\"0.1\"), Decimal(\"0.2\"), Decimal(\"0.3\"), Decimal(\"0.4\"), Decimal(\"0.5\"), Decimal(\"0.6\")]\n",
+ "\n",
+ "# Triple barrier configuration\n",
+ "stop_loss = Decimal(\"0.015\")\n",
+ "take_profit = Decimal(\"0.03\")\n",
+ "time_limit = 60 * 60 * 12 # 12 hours\n",
+ "trailing_stop_activation_price_delta = Decimal(\"0.008\")\n",
+ "trailing_stop_trailing_delta = Decimal(\"0.004\")"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-05-21T23:53:21.058600Z",
+ "start_time": "2024-05-21T23:53:21.024538Z"
+ }
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
+ "ExecuteTime": {
+ "end_time": "2024-05-21T23:53:21.062445Z",
+ "start_time": "2024-05-21T23:53:21.029893Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from hummingbot.strategy_v2.executors.position_executor.data_types import TrailingStop\n",
+ "from quants_lab.controllers.market_making.dman_maker_v2 import DManMakerV2Config, DManMakerV2\n",
+ "\n",
+ "\n",
+ "# Creating the instance of the configuration and the controller\n",
+ "config = DManMakerV2Config(\n",
+ " connector_name=connector_name,\n",
+ " trading_pair=trading_pair,\n",
+ " total_amount_quote=total_amount_quote,\n",
+ " buy_spreads=buy_spreads,\n",
+ " sell_spreads=sell_spreads,\n",
+ " buy_amounts_pct=buy_amounts_pct,\n",
+ " sell_amounts_pct=sell_amounts_pct,\n",
+ " executor_refresh_time=executor_refresh_time,\n",
+ " cooldown_time=cooldown_time,\n",
+ " top_executor_refresh_time=top_executor_refresh_time,\n",
+ " executor_activation_bounds=executor_activation_bounds,\n",
+ " stop_loss=stop_loss,\n",
+ " take_profit=take_profit,\n",
+ " time_limit=time_limit,\n",
+ " trailing_stop=TrailingStop(activation_price=trailing_stop_activation_price_delta,\n",
+ " trailing_delta=trailing_stop_trailing_delta),\n",
+ " dca_spreads=dca_spreads,\n",
+ " dca_amounts=dca_amounts\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 47,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
+ "ExecuteTime": {
+ "end_time": "2024-05-21T23:53:53.099177Z",
+ "start_time": "2024-05-21T23:53:39.419416Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "2024-05-21 18:53:41,626 - asyncio - ERROR - Unclosed client session\n",
+ "client_session: \n",
+ "2024-05-21 18:53:41,627 - asyncio - ERROR - Unclosed connector\n",
+ "connections: ['[(, 55330.4882885)]']\n",
+ "connector: \n"
+ ]
+ }
+ ],
+ "source": [
+ "import pandas as pd\n",
+ "from hummingbot.strategy_v2.backtesting import MarketMakingBacktesting\n",
+ "\n",
+ "backtesting_engine = MarketMakingBacktesting()\n",
+ "\n",
+ "# Backtest period\n",
+ "start = \"2024-01-01\"\n",
+ "end = \"2024-01-10\"\n",
+ "backtesting_resolution = \"1m\"\n",
+ "\n",
+ "start_time = pd.to_datetime(start).timestamp() * 1e3\n",
+ "end_time = pd.to_datetime(end).timestamp() * 1e3\n",
+ "backtesting_results = await backtesting_engine.run_backtesting(\n",
+ " controller_config=config, trade_cost=trade_cost,\n",
+ " start=int(start_time), end=int(end_time),\n",
+ " backtesting_resolution=backtesting_resolution)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 48,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
+ "ExecuteTime": {
+ "end_time": "2024-05-21T23:53:53.105729Z",
+ "start_time": "2024-05-21T23:53:53.098438Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": "dict_keys(['executors', 'results', 'processed_data'])"
+ },
+ "execution_count": 48,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Let's see what is inside the backtesting results\n",
+ "backtesting_results.keys()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 49,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
+ "ExecuteTime": {
+ "end_time": "2024-05-21T23:53:53.107299Z",
+ "start_time": "2024-05-21T23:53:53.104827Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": "{'reference_price': Decimal('2.497599999999999820232687852694652974605560302734375'),\n 'spread_multiplier': Decimal('1'),\n 'features': timestamp_bt open_bt high_bt low_bt close_bt volume_bt \\\n 0 1704067200000 3.6272 3.6335 3.6268 3.6335 1247.4443 \n 1 1704067260000 3.6333 3.639 3.6317 3.639 373.5355 \n 2 1704067320000 3.6476 3.657 3.6476 3.657 504.2724 \n 3 1704067380000 3.6601 3.6622 3.6586 3.6622 43.5494 \n 4 1704067440000 3.6622 3.6622 3.6622 3.6622 0 \n ... ... ... ... ... ... ... \n 12964 1704844560000 2.4965 2.4965 2.495 2.495 5.1705 \n 12965 1704844620000 2.4978 2.5025 2.4978 2.5025 37.5497 \n 12966 1704844680000 2.5025 2.5025 2.5025 2.5025 0 \n 12967 1704844740000 2.5027 2.5034 2.5006 2.5006 63.5077 \n 12968 1704844800000 2.5026 2.5026 2.4976 2.4976 1009.2771 \n \n quote_asset_volume_bt n_trades_bt taker_buy_base_volume_bt \\\n 0 4528.54482718 0 0 \n 1 1357.28548746 0 0 \n 2 1843.41100357 0 0 \n 3 159.35951529 0 0 \n 4 0 0 0 \n ... ... ... ... \n 12964 12.9018441 0 0 \n 12965 93.93699135 0 0 \n 12966 0 0 0 \n 12967 158.88729718 0 0 \n 12968 2523.49246652 0 0 \n \n taker_buy_quote_volume_bt reference_price spread_multiplier signal \\\n 0 0 3.6335 1 0 \n 1 0 3.639 1 0 \n 2 0 3.657 1 0 \n 3 0 3.6622 1 0 \n 4 0 3.6622 1 0 \n ... ... ... ... ... \n 12964 0 2.495 1 0 \n 12965 0 2.5025 1 0 \n 12966 0 2.5025 1 0 \n 12967 0 2.5006 1 0 \n 12968 0 2.4976 1 0 \n \n timestamp open high low close volume \n 0 1704067200000 3.6272 3.6335 3.6268 3.6335 1247.4443 \n 1 1704067260000 3.6333 3.639 3.6317 3.639 373.5355 \n 2 1704067320000 3.6476 3.657 3.6476 3.657 504.2724 \n 3 1704067380000 3.6601 3.6622 3.6586 3.6622 43.5494 \n 4 1704067440000 3.6622 3.6622 3.6622 3.6622 0 \n ... ... ... ... ... ... ... \n 12964 1704844560000 2.4965 2.4965 2.495 2.495 5.1705 \n 12965 1704844620000 2.4978 2.5025 2.4978 2.5025 37.5497 \n 12966 1704844680000 2.5025 2.5025 2.5025 2.5025 0 \n 12967 1704844740000 2.5027 2.5034 2.5006 2.5006 63.5077 \n 12968 1704844800000 2.5026 2.5026 2.4976 2.4976 1009.2771 \n \n [12961 rows x 19 columns]}"
+ },
+ "execution_count": 49,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# now let's analyze each of the dataframes\n",
+ "\n",
+ "# 1. The processed data: this is the data that we are going to use to generate the signal\n",
+ "backtesting_results[\"processed_data\"]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 50,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
+ "ExecuteTime": {
+ "end_time": "2024-05-21T23:53:53.140339Z",
+ "start_time": "2024-05-21T23:53:53.117236Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": "[ExecutorInfo(id='ATh9By7hcv5V8pLarj4Cy69kKuYgBpNYag1QS428cgjc', timestamp=1704067200000.0, type='dca_executor', close_timestamp=1704067320000.0, close_type=, status=, config=DCAExecutorConfig(id='ATh9By7hcv5V8pLarj4Cy69kKuYgBpNYag1QS428cgjc', type='dca_executor', timestamp=1704067200000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380955'), Decimal('70.85714285714285714285714287'), Decimal('94.28571428571428571428571430'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.626233000000000173583529495'), Decimal('3.618980534000000173236362436'), Decimal('3.597223136000000172194861259'), Decimal('3.589970670000000171847694200'), Decimal('3.571839505000000170979776553'), Decimal('3.553708340000000170111858905')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.657, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.626233}, controller_id=None),\n ExecutorInfo(id='J5kLBeH8TTiQRd1iMTnEG9hHRwp44sBRbgta7PhkP7U4', timestamp=1704067200000.0, type='dca_executor', close_timestamp=1704072300000.0, close_type=, status=, config=DCAExecutorConfig(id='J5kLBeH8TTiQRd1iMTnEG9hHRwp44sBRbgta7PhkP7U4', type='dca_executor', timestamp=1704067200000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428573'), Decimal('71.99999999999999999999999999'), Decimal('96.19047619047619047619047617'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.640767000000000174582411026'), Decimal('3.648048534000000174931575848'), Decimal('3.669893136000000175979070314'), Decimal('3.677174670000000176328235136'), Decimal('3.695378505000000177201147191'), Decimal('3.713582340000000178074059247')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('-0.01516281491309645797216365536996818264015018939971923828125'), net_pnl_quote=Decimal('-7.67635555945642789055227694916538894176483154296875'), cum_fees_quote=Decimal('0.3037571428571428366893769634771160781383514404296875'), filled_amount_quote=Decimal('506.26190476190475919793243519961833953857421875'), is_active=False, is_trading=False, custom_info={'close_price': 3.7496, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.686505601555519}, controller_id=None),\n ExecutorInfo(id='FbqtGEUMzuDrG1G9cEP3aV31rLQwjWGxLoyV7TnDeDXs', timestamp=1704067380000.0, type='dca_executor', close_timestamp=1704072360000.0, close_type=, status=, config=DCAExecutorConfig(id='FbqtGEUMzuDrG1G9cEP3aV31rLQwjWGxLoyV7TnDeDXs', type='dca_executor', timestamp=1704067380000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380955'), Decimal('70.85714285714285714285714286'), Decimal('94.28571428571428571428571429'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.654875599999999899861377606'), Decimal('3.647565848799999900061654851'), Decimal('3.625636595199999900662486585'), Decimal('3.618326843999999900862763830'), Decimal('3.600052465999999901363456942'), Decimal('3.581778087999999901864150054')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0.033093745264367972758190461490812595002353191375732421875'), net_pnl_quote=Decimal('2.36068716219158236668818062753416597843170166015625'), cum_fees_quote=Decimal('0.042800000000000004707345624410663731396198272705078125'), filled_amount_quote=Decimal('71.333333333333342807236476801335811614990234375'), is_active=False, is_trading=False, custom_info={'close_price': 3.7679, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.650005685649132}, controller_id=None),\n ExecutorInfo(id='HVMBhGCSPyoW2MxZEFeKyq5CmdqEDVmaYz4Up44JrfV9', timestamp=1704075900000.0, type='dca_executor', close_timestamp=1704076020000.0, close_type=, status=, config=DCAExecutorConfig(id='HVMBhGCSPyoW2MxZEFeKyq5CmdqEDVmaYz4Up44JrfV9', type='dca_executor', timestamp=1704075900000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428572'), Decimal('72.00000000000000000000000002'), Decimal('96.19047619047619047619047620'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.688762800000000005848979632'), Decimal('3.696140325600000005860677591'), Decimal('3.718272902400000005895771469'), Decimal('3.725650428000000005907469428'), Decimal('3.744094242000000005936714326'), Decimal('3.762538056000000005965959225')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6849, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6887628}, controller_id=None),\n ExecutorInfo(id='Giv5Y3r8oT7THrkusS6Pjef5hU8Wc5rHhiy47RugTyTa', timestamp=1704076080000.0, type='dca_executor', close_timestamp=1704076200000.0, close_type=, status=, config=DCAExecutorConfig(id='Giv5Y3r8oT7THrkusS6Pjef5hU8Wc5rHhiy47RugTyTa', type='dca_executor', timestamp=1704076080000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428571'), Decimal('72.00000000000000000000000003'), Decimal('96.19047619047619047619047622'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.693772799999999899054614614'), Decimal('3.701160345599999898852723843'), Decimal('3.723322982399999898247051531'), Decimal('3.730710527999999898045160760'), Decimal('3.749179391999999897540433833'), Decimal('3.767648255999999897035706906')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6843, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6937728}, controller_id=None),\n ExecutorInfo(id='A2nCeBXQfU9WT8xQkW9fkuS1EUqQaqaUCeX5R5XkLXbd', timestamp=1704076260000.0, type='dca_executor', close_timestamp=1704076380000.0, close_type=, status=, config=DCAExecutorConfig(id='A2nCeBXQfU9WT8xQkW9fkuS1EUqQaqaUCeX5R5XkLXbd', type='dca_executor', timestamp=1704076260000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952380'), Decimal('47.71428571428571428571428570'), Decimal('71.99999999999999999999999998'), Decimal('96.19047619047619047619047617'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.688362000000000049990719895'), Decimal('3.695738724000000050090701335'), Decimal('3.717868896000000050390645654'), Decimal('3.725245620000000050490627094'), Decimal('3.743687430000000050740580693'), Decimal('3.762129240000000050990534293')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6783, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.688362}, controller_id=None),\n ExecutorInfo(id='EpHqffK2HVxQJZnwy3DtiFAfpioZH4X2KDGTQvDxuZE2', timestamp=1704076440000.0, type='dca_executor', close_timestamp=1704076560000.0, close_type=, status=, config=DCAExecutorConfig(id='EpHqffK2HVxQJZnwy3DtiFAfpioZH4X2KDGTQvDxuZE2', type='dca_executor', timestamp=1704076440000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952380'), Decimal('47.71428571428571428571428571'), Decimal('72.00000000000000000000000000'), Decimal('96.19047619047619047619047618'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.685957199999999869863773204'), Decimal('3.693329114399999869603500750'), Decimal('3.715444857599999868822683390'), Decimal('3.722816771999999868562410936'), Decimal('3.741246557999999867911729802'), Decimal('3.759676343999999867261048668')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6786, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6859572}, controller_id=None),\n ExecutorInfo(id='76VV1Dj9mDBg7vpAsSR1i55gbndn1Wds9auLpiAd6CQk', timestamp=1704076620000.0, type='dca_executor', close_timestamp=1704076740000.0, close_type=, status=, config=DCAExecutorConfig(id='76VV1Dj9mDBg7vpAsSR1i55gbndn1Wds9auLpiAd6CQk', type='dca_executor', timestamp=1704076620000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428572'), Decimal('72.00000000000000000000000002'), Decimal('96.19047619047619047619047623'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142858')], prices=[Decimal('3.680546400000000020799878485'), Decimal('3.687907492800000020841478242'), Decimal('3.709990771200000020966277513'), Decimal('3.717351864000000021007877270'), Decimal('3.735754596000000021111876662'), Decimal('3.754157328000000021215876055')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6715, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6805464}, controller_id=None),\n ExecutorInfo(id='E3vRDqYQbgm6YFYYxqDAjd5Wcy4SRZYJ11AvibZbHVTU', timestamp=1704072360000.0, type='dca_executor', close_timestamp=1704076860000.0, close_type=, status=, config=DCAExecutorConfig(id='E3vRDqYQbgm6YFYYxqDAjd5Wcy4SRZYJ11AvibZbHVTU', type='dca_executor', timestamp=1704072360000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380952'), Decimal('70.85714285714285714285714289'), Decimal('94.28571428571428571428571431'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.760364200000000026789752361'), Decimal('3.752843471600000026736172856'), Decimal('3.730281286400000026575434342'), Decimal('3.722760558000000026521854837'), Decimal('3.703958737000000026387906076'), Decimal('3.685156916000000026253957314')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('-0.0145037687138440223033608589275900158099830150604248046875'), net_pnl_quote=Decimal('-7.1610631385472256482671582489274442195892333984375'), cum_fees_quote=Decimal('0.29624285714285714110616254401975311338901519775390625'), filled_amount_quote=Decimal('493.73809523809524080206756480038166046142578125'), is_active=False, is_trading=False, custom_info={'close_price': 3.6552, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.713420848392969}, controller_id=None),\n ExecutorInfo(id='2oEWXZQv7ewiAUt6VRU5VHLqBgJc3vJXVU3SCjS9FyxT', timestamp=1704076800000.0, type='dca_executor', close_timestamp=1704076920000.0, close_type=, status=, config=DCAExecutorConfig(id='2oEWXZQv7ewiAUt6VRU5VHLqBgJc3vJXVU3SCjS9FyxT', type='dca_executor', timestamp=1704076800000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952382'), Decimal('47.71428571428571428571428573'), Decimal('72.00000000000000000000000006'), Decimal('96.19047619047619047619047624'), Decimal('120.8333333333333333333333334'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.676137600000000061381633108'), Decimal('3.683489875200000061504396374'), Decimal('3.705546700800000061872686173'), Decimal('3.712898976000000061995449439'), Decimal('3.731279664000000062302357605'), Decimal('3.749660352000000062609265770')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6576, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6761376}, controller_id=None),\n ExecutorInfo(id='6GmfMuYpmpcoZ8S4qsdwsNrGFLbykxRTPjA4MRjho8YE', timestamp=1704076980000.0, type='dca_executor', close_timestamp=1704077100000.0, close_type=, status=, config=DCAExecutorConfig(id='6GmfMuYpmpcoZ8S4qsdwsNrGFLbykxRTPjA4MRjho8YE', type='dca_executor', timestamp=1704076980000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952382'), Decimal('47.71428571428571428571428573'), Decimal('72.00000000000000000000000006'), Decimal('96.19047619047619047619047622'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142858')], prices=[Decimal('3.654995399999999942482796498'), Decimal('3.662305390799999942367762091'), Decimal('3.684235363199999942022658870'), Decimal('3.691545353999999941907624463'), Decimal('3.709820330999999941620038445'), Decimal('3.728095307999999941332452428')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6333, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6549954}, controller_id=None),\n ExecutorInfo(id='C3gb5NYU3hcdJfzUvzTaLwXK8t7ZqtvgkC7jNGkBaB3b', timestamp=1704077160000.0, type='dca_executor', close_timestamp=1704077280000.0, close_type=, status=, config=DCAExecutorConfig(id='C3gb5NYU3hcdJfzUvzTaLwXK8t7ZqtvgkC7jNGkBaB3b', type='dca_executor', timestamp=1704077160000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428570'), Decimal('72.00000000000000000000000004'), Decimal('96.19047619047619047619047622'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.639364199999999884101113677'), Decimal('3.646642928399999883869315904'), Decimal('3.668479113599999883173922586'), Decimal('3.675757841999999882942124814'), Decimal('3.693954662999999882362630382'), Decimal('3.712151483999999881783135951')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6304, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6393641999999997}, controller_id=None),\n ExecutorInfo(id='2Q78Zgrg28Vj1zxYs4RaawWc1uJBbAS1vkkdjQdB9n7i', timestamp=1704077340000.0, type='dca_executor', close_timestamp=1704109320000.0, close_type=, status=, config=DCAExecutorConfig(id='2Q78Zgrg28Vj1zxYs4RaawWc1uJBbAS1vkkdjQdB9n7i', type='dca_executor', timestamp=1704077340000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952382'), Decimal('47.71428571428571428571428571'), Decimal('72.00000000000000000000000002'), Decimal('96.19047619047619047619047623'), Decimal('120.8333333333333333333333334'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.637059599999999915427426054'), Decimal('3.644333719199999915258280906'), Decimal('3.666156076799999914750845462'), Decimal('3.673430195999999914581700315'), Decimal('3.691615493999999914158837445'), Decimal('3.709800791999999913735974575')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0.031084693949450438454906731067239888943731784820556640625'), net_pnl_quote=Decimal('2.22329572914640749559112009592354297637939453125'), cum_fees_quote=Decimal('0.0429142857142857103713851074644480831921100616455078125'), filled_amount_quote=Decimal('71.5238095238095183958648703992366790771484375'), is_active=False, is_trading=False, custom_info={'close_price': 3.5315, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.641912241437017}, controller_id=None),\n ExecutorInfo(id='AzHcWY7a6BZcEZL1YHHwoZAYq8bSKe9zQQ68nBCBnzCt', timestamp=1704109320000.0, type='dca_executor', close_timestamp=1704109440000.0, close_type=, status=, config=DCAExecutorConfig(id='AzHcWY7a6BZcEZL1YHHwoZAYq8bSKe9zQQ68nBCBnzCt', type='dca_executor', timestamp=1704109320000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428571'), Decimal('72.00000000000000000000000006'), Decimal('96.19047619047619047619047622'), Decimal('120.8333333333333333333333334'), Decimal('145.7142857142857142857142858')], prices=[Decimal('3.538562999999999861314083081'), Decimal('3.545640125999999861036711247'), Decimal('3.566871503999999860204595746'), Decimal('3.573948629999999859927223912'), Decimal('3.591641444999999859233794327'), Decimal('3.609334259999999858540364743')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.5385, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.538563}, controller_id=None),\n ExecutorInfo(id='G676QHWqXVGeBr7MtEw7BCkgJsy1v8gZ6QFUrYouBrBy', timestamp=1704109500000.0, type='dca_executor', close_timestamp=1704109620000.0, close_type=, status=, config=DCAExecutorConfig(id='G676QHWqXVGeBr7MtEw7BCkgJsy1v8gZ6QFUrYouBrBy', type='dca_executor', timestamp=1704109500000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952382'), Decimal('47.71428571428571428571428571'), Decimal('72.00000000000000000000000005'), Decimal('96.19047619047619047619047624'), Decimal('120.8333333333333333333333334'), Decimal('145.7142857142857142857142858')], prices=[Decimal('3.548182200000000136844481578'), Decimal('3.555278564400000137118170541'), Decimal('3.576567657600000137939237431'), Decimal('3.583664022000000138212926394'), Decimal('3.601404933000000138897148802'), Decimal('3.619145844000000139581371210')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.5404, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.5481822000000003}, controller_id=None),\n ExecutorInfo(id='78aQMv4CM5ZX2qK8cpK4kaZMG9p5LfgaST4upkumjHDe', timestamp=1704109680000.0, type='dca_executor', close_timestamp=1704109800000.0, close_type=, status=, config=DCAExecutorConfig(id='78aQMv4CM5ZX2qK8cpK4kaZMG9p5LfgaST4upkumjHDe', type='dca_executor', timestamp=1704109680000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952380'), Decimal('47.71428571428571428571428571'), Decimal('71.99999999999999999999999999'), Decimal('96.19047619047619047619047617'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.540567000000000085582770036'), Decimal('3.547648134000000085753935576'), Decimal('3.568891536000000086267432196'), Decimal('3.575972670000000086438597736'), Decimal('3.593675505000000086866511587'), Decimal('3.611378340000000087294425437')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.5366, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.5405670000000002}, controller_id=None),\n ExecutorInfo(id='DMuck9wctakrNJ2sZ3qNiJ4oNxZvqJ9SPcFDEtoFq7bf', timestamp=1704109860000.0, type='dca_executor', close_timestamp=1704109980000.0, close_type=, status=, config=DCAExecutorConfig(id='DMuck9wctakrNJ2sZ3qNiJ4oNxZvqJ9SPcFDEtoFq7bf', type='dca_executor', timestamp=1704109860000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428571'), Decimal('72.00000000000000000000000000'), Decimal('96.19047619047619047619047619'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.543673199999999965972977132'), Decimal('3.550760546399999965904923086'), Decimal('3.572022585599999965700760949'), Decimal('3.579109931999999965632706903'), Decimal('3.596828297999999965462571789'), Decimal('3.614546663999999965292436675')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.539, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.5436731999999997}, controller_id=None),\n ExecutorInfo(id='9oSUcrrAE3MccCqVK7Hd4rJr2fcq4QLopXju24y6RXnc', timestamp=1704080460000.0, type='dca_executor', close_timestamp=1704123660000.0, close_type=, status=, config=DCAExecutorConfig(id='9oSUcrrAE3MccCqVK7Hd4rJr2fcq4QLopXju24y6RXnc', type='dca_executor', timestamp=1704080460000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380954'), Decimal('70.85714285714285714285714288'), Decimal('94.28571428571428571428571430'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.569446800000000001269337413'), Decimal('3.562307906400000001266798738'), Decimal('3.540891225600000001259182714'), Decimal('3.533752332000000001256644039'), Decimal('3.515905098000000001250297352'), Decimal('3.498057864000000001243950665')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('-0.00023754063515637728761474567473754859747714363038539886474609375'), net_pnl_quote=Decimal('-0.117282860743757044730273264576680958271026611328125'), cum_fees_quote=Decimal('0.29624285714285714110616254401975311338901519775390625'), filled_amount_quote=Decimal('493.73809523809524080206756480038166046142578125'), is_active=False, is_trading=False, custom_info={'close_price': 3.5185, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.524886808663259}, controller_id=None),\n ExecutorInfo(id='8Tkirv9ee195HKe5iLoEucwThiHVD5oaa5zVkqXFkMCy', timestamp=1704123660000.0, type='dca_executor', close_timestamp=1704123780000.0, close_type=, status=, config=DCAExecutorConfig(id='8Tkirv9ee195HKe5iLoEucwThiHVD5oaa5zVkqXFkMCy', type='dca_executor', timestamp=1704123660000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952380'), Decimal('47.52380952380952380952380953'), Decimal('70.85714285714285714285714287'), Decimal('94.28571428571428571428571429'), Decimal('117.2619047619047619047619047'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.511462999999999960851822245'), Decimal('3.504440073999999960930118601'), Decimal('3.483371295999999961165007667'), Decimal('3.476348369999999961243304023'), Decimal('3.458791054999999961439044911'), Decimal('3.441233739999999961634785800')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.5228, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.511463}, controller_id=None),\n ExecutorInfo(id='3A2bsXXCUsutNgULychab5goCkaRvWoZSzdmjbfzhk4U', timestamp=1704123840000.0, type='dca_executor', close_timestamp=1704123960000.0, close_type=, status=, config=DCAExecutorConfig(id='3A2bsXXCUsutNgULychab5goCkaRvWoZSzdmjbfzhk4U', type='dca_executor', timestamp=1704123840000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380954'), Decimal('70.85714285714285714285714288'), Decimal('94.28571428571428571428571428'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.525135600000000006235037486'), Decimal('3.518085328800000006222567411'), Decimal('3.496934515200000006185157186'), Decimal('3.489884244000000006172687111'), Decimal('3.472258566000000006141511924'), Decimal('3.454632888000000006110336736')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.5363, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.5251356}, controller_id=None),\n ExecutorInfo(id='3ZGoQ6gtLd49DNDuMbYTkJs9cXGgpPR7MRLxbzCxWbeA', timestamp=1704124020000.0, type='dca_executor', close_timestamp=1704139260000.0, close_type=, status=, config=DCAExecutorConfig(id='3ZGoQ6gtLd49DNDuMbYTkJs9cXGgpPR7MRLxbzCxWbeA', type='dca_executor', timestamp=1704124020000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380952'), Decimal('70.85714285714285714285714291'), Decimal('94.28571428571428571428571430'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.529227400000000220389605177'), Decimal('3.522168945200000219948825967'), Decimal('3.500993580800000218626488336'), Decimal('3.493935126000000218185709125'), Decimal('3.476288989000000217083761099'), Decimal('3.458642852000000215981813073')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0.0308962131428041804348527676893354509957134723663330078125'), net_pnl_quote=Decimal('7.30621878415074110790783379343338310718536376953125'), cum_fees_quote=Decimal('0.14188571428571428167941803621943108737468719482421875'), filled_amount_quote=Decimal('236.4761904761904816041351296007633209228515625'), is_active=False, is_trading=False, custom_info={'close_price': 3.6117, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.5052775483326624}, controller_id=None),\n ExecutorInfo(id='HfeBhgPMXUN7FEf9X2hYcEn1vDgmCZ78LQ1igd48eYYM', timestamp=1704139260000.0, type='dca_executor', close_timestamp=1704139380000.0, close_type=, status=, config=DCAExecutorConfig(id='HfeBhgPMXUN7FEf9X2hYcEn1vDgmCZ78LQ1igd48eYYM', type='dca_executor', timestamp=1704139260000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952380'), Decimal('47.52380952380952380952380950'), Decimal('70.85714285714285714285714284'), Decimal('94.28571428571428571428571429'), Decimal('117.2619047619047619047619047'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.604476599999999910500304845'), Decimal('3.597267646799999910679304235'), Decimal('3.575640787199999911216302406'), Decimal('3.568431833999999911395301797'), Decimal('3.550409450999999911842800272'), Decimal('3.532387067999999912290298748')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6112, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.6044766}, controller_id=None),\n ExecutorInfo(id='4RRTfhyXDBSH4SpAt5eCNoRwAsCBcgrCnjUQNjTRDocb', timestamp=1704139440000.0, type='dca_executor', close_timestamp=1704139560000.0, close_type=, status=, config=DCAExecutorConfig(id='4RRTfhyXDBSH4SpAt5eCNoRwAsCBcgrCnjUQNjTRDocb', type='dca_executor', timestamp=1704139440000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380953'), Decimal('70.85714285714285714285714288'), Decimal('94.28571428571428571428571429'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.598887800000000082817099012'), Decimal('3.591690024400000082651464814'), Decimal('3.570096697600000082154562220'), Decimal('3.562898922000000081988928022'), Decimal('3.544904483000000081574842527'), Decimal('3.526910044000000081160757032')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6138, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.5988878}, controller_id=None),\n ExecutorInfo(id='CN8X5KdydpXCEYWBSxvWVFSbai96m8TYai2TJNaSnB4L', timestamp=1704139620000.0, type='dca_executor', close_timestamp=1704139740000.0, close_type=, status=, config=DCAExecutorConfig(id='CN8X5KdydpXCEYWBSxvWVFSbai96m8TYai2TJNaSnB4L', type='dca_executor', timestamp=1704139620000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952380'), Decimal('47.52380952380952380952380952'), Decimal('70.85714285714285714285714287'), Decimal('94.28571428571428571428571428'), Decimal('117.2619047619047619047619047'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.607869799999999979994085020'), Decimal('3.600654060399999980034096850'), Decimal('3.579006841599999980154132340'), Decimal('3.571791101999999980194144170'), Decimal('3.553751752999999980294173745'), Decimal('3.535712403999999980394203320')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6148, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.6078698}, controller_id=None),\n ExecutorInfo(id='9Hbf6s4dUzibQBzKGtxBYiXmgaFFHa8SxXdwky3rhK2a', timestamp=1704139800000.0, type='dca_executor', close_timestamp=1704139920000.0, close_type=, status=, config=DCAExecutorConfig(id='9Hbf6s4dUzibQBzKGtxBYiXmgaFFHa8SxXdwky3rhK2a', type='dca_executor', timestamp=1704139800000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380954'), Decimal('70.85714285714285714285714288'), Decimal('94.28571428571428571428571428'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.607370799999999813350518019'), Decimal('3.600156058399999813723816983'), Decimal('3.578511833599999814843713875'), Decimal('3.571297091999999815217012839'), Decimal('3.553260237999999816150260249'), Decimal('3.535223383999999817083507659')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6124, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.6073708}, controller_id=None),\n ExecutorInfo(id='EDn4KZGBAuJkiEJ6tZ6CofT6DqUgZ8wsuTyNNgq5gEYK', timestamp=1704110040000.0, type='dca_executor', close_timestamp=1704153240000.0, close_type=, status=, config=DCAExecutorConfig(id='EDn4KZGBAuJkiEJ6tZ6CofT6DqUgZ8wsuTyNNgq5gEYK', type='dca_executor', timestamp=1704110040000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428571'), Decimal('72.00000000000000000000000004'), Decimal('96.19047619047619047619047617'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.546078000000000146099923824'), Decimal('3.553170156000000146392123672'), Decimal('3.574446624000000147268723215'), Decimal('3.581538780000000147560923062'), Decimal('3.599269170000000148291422681'), Decimal('3.616999560000000149021922300')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('-0.00854299774488135633954133396628094487823545932769775390625'), net_pnl_quote=Decimal('-4.3249943107002923881054812227375805377960205078125'), cum_fees_quote=Decimal('0.3037571428571428366893769634771160781383514404296875'), filled_amount_quote=Decimal('506.26190476190475919793243519961833953857421875'), is_active=False, is_trading=False, custom_info={'close_price': 3.6245, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.590627032862249}, controller_id=None),\n ExecutorInfo(id='9ZcJRETDNT2qt5zqyGAfdStFRB4eqj5y9FVVfsJJMVVd', timestamp=1704153240000.0, type='dca_executor', close_timestamp=1704153360000.0, close_type=, status=, config=DCAExecutorConfig(id='9ZcJRETDNT2qt5zqyGAfdStFRB4eqj5y9FVVfsJJMVVd', type='dca_executor', timestamp=1704153240000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428572'), Decimal('71.99999999999999999999999999'), Decimal('96.19047619047619047619047616'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.631748999999999832839402135'), Decimal('3.639012497999999832505080939'), Decimal('3.660802991999999831502117352'), Decimal('3.668066489999999831167796156'), Decimal('3.686225234999999830331993167'), Decimal('3.704383979999999829496190178')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6174, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6317489999999997}, controller_id=None),\n ExecutorInfo(id='7qv4cbX8wcYNQoBzjKFX8KNneQ8jwtePgwbZdDCbpTP8', timestamp=1704153420000.0, type='dca_executor', close_timestamp=1704153540000.0, close_type=, status=, config=DCAExecutorConfig(id='7qv4cbX8wcYNQoBzjKFX8KNneQ8jwtePgwbZdDCbpTP8', type='dca_executor', timestamp=1704153420000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428572'), Decimal('72.00000000000000000000000002'), Decimal('96.19047619047619047619047622'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.624735000000000160342468468'), Decimal('3.631984470000000160663153405'), Decimal('3.653732880000000161625208216'), Decimal('3.660982350000000161945893153'), Decimal('3.679106025000000162747605495'), Decimal('3.697229700000000163549317837')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6119, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6247350000000003}, controller_id=None),\n ExecutorInfo(id='CYAt1pa4TTc4goAfMXC9NA4PUuSpudVf4JCoujG6Pv5U', timestamp=1704153600000.0, type='dca_executor', close_timestamp=1704153720000.0, close_type=, status=, config=DCAExecutorConfig(id='CYAt1pa4TTc4goAfMXC9NA4PUuSpudVf4JCoujG6Pv5U', type='dca_executor', timestamp=1704153600000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428572'), Decimal('72.00000000000000000000000001'), Decimal('96.19047619047619047619047620'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.613512600000000061379031023'), Decimal('3.620739625200000061501789085'), Decimal('3.642420700800000061870063271'), Decimal('3.649647726000000061992821333'), Decimal('3.667715289000000062299716488'), Decimal('3.685782852000000062606611643')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.599, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6135126}, controller_id=None),\n ExecutorInfo(id='HbP6YqUFhpxnFWdEw9N2uviWQ7TVuWWuKKnW3q75KTv9', timestamp=1704139980000.0, type='dca_executor', close_timestamp=1704160620000.0, close_type=, status=, config=DCAExecutorConfig(id='HbP6YqUFhpxnFWdEw9N2uviWQ7TVuWWuKKnW3q75KTv9', type='dca_executor', timestamp=1704139980000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380953'), Decimal('70.85714285714285714285714287'), Decimal('94.28571428571428571428571430'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.606771999999999879298856476'), Decimal('3.599558455999999879540258763'), Decimal('3.577917823999999880264465624'), Decimal('3.570704279999999880505867911'), Decimal('3.552670419999999881109373629'), Decimal('3.534636559999999881712879346')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0.03145933504437083139126940523055964149534702301025390625'), net_pnl_quote=Decimal('7.4393837062069305687828091322444379329681396484375'), cum_fees_quote=Decimal('0.14188571428571428167941803621943108737468719482421875'), filled_amount_quote=Decimal('236.4761904761904816041351296007633209228515625'), is_active=False, is_trading=False, custom_info={'close_price': 3.6908, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.5822959193717274}, controller_id=None),\n ExecutorInfo(id='BCwKiF9hHYvyG4uiyZVrsiGYBLY2bsktMPBZqaa2gR5U', timestamp=1704153780000.0, type='dca_executor', close_timestamp=1704161760000.0, close_type=, status=, config=DCAExecutorConfig(id='BCwKiF9hHYvyG4uiyZVrsiGYBLY2bsktMPBZqaa2gR5U', type='dca_executor', timestamp=1704153780000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428573'), Decimal('72.00000000000000000000000002'), Decimal('96.19047619047619047619047624'), Decimal('120.8333333333333333333333334'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.603692999999999807919502657'), Decimal('3.610900385999999807535341662'), Decimal('3.632522543999999806382858678'), Decimal('3.639729929999999805998697684'), Decimal('3.657748394999999805038295197'), Decimal('3.675766859999999804077892710')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('-0.01493250714901908811349517947064668987877666950225830078125'), net_pnl_quote=Decimal('-7.55975951213316310628442806773819029331207275390625'), cum_fees_quote=Decimal('0.3037571428571428366893769634771160781383514404296875'), filled_amount_quote=Decimal('506.26190476190475919793243519961833953857421875'), is_active=False, is_trading=False, custom_info={'close_price': 3.706, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.648965844501011}, controller_id=None),\n ExecutorInfo(id='mxTQ9PYU7EkQQaY7BLcFwcLxT4HtQTQRdYQxSxwkx9V', timestamp=1704160620000.0, type='dca_executor', close_timestamp=1704177900000.0, close_type=, status=, config=DCAExecutorConfig(id='mxTQ9PYU7EkQQaY7BLcFwcLxT4HtQTQRdYQxSxwkx9V', type='dca_executor', timestamp=1704160620000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380955'), Decimal('70.85714285714285714285714292'), Decimal('94.28571428571428571428571433'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000001')], prices=[Decimal('3.683418399999999858731131175'), Decimal('3.676051563199999859013668913'), Decimal('3.653951052799999859861282126'), Decimal('3.646584215999999860143819863'), Decimal('3.628167123999999860850164207'), Decimal('3.609750031999999861556508552')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0.032656674102879611198968490270999609492719173431396484375'), net_pnl_quote=Decimal('2.32950941933874577927099380758590996265411376953125'), cum_fees_quote=Decimal('0.042800000000000004707345624410663731396198272705078125'), filled_amount_quote=Decimal('71.333333333333342807236476801335811614990234375'), is_active=False, is_trading=False, custom_info={'close_price': 3.7946, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.678510453987717}, controller_id=None),\n ExecutorInfo(id='3gqAmdWvLCPHMXCsKA3MZ5JemnTMF4Q7SiR6mdmQYmZq', timestamp=1704165360000.0, type='dca_executor', close_timestamp=1704189000000.0, close_type=, status=, config=DCAExecutorConfig(id='3gqAmdWvLCPHMXCsKA3MZ5JemnTMF4Q7SiR6mdmQYmZq', type='dca_executor', timestamp=1704165360000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428574'), Decimal('72.00000000000000000000000001'), Decimal('96.19047619047619047619047620'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.743271599999999787278351369'), Decimal('3.750758143199999786852908072'), Decimal('3.773217772799999785576578180'), Decimal('3.780704315999999785151134883'), Decimal('3.799420673999999784087526640'), Decimal('3.818137031999999783023918396')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('-0.01390103960291601097043479029480295139364898204803466796875'), net_pnl_quote=Decimal('-7.037566787542932189580824342556297779083251953125'), cum_fees_quote=Decimal('0.3037571428571428366893769634771160781383514404296875'), filled_amount_quote=Decimal('506.26190476190475919793243519961833953857421875'), is_active=False, is_trading=False, custom_info={'close_price': 3.8476, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.7902979568710906}, controller_id=None),\n ExecutorInfo(id='8TA9qPcksvZ9gtDM31BMdJiVsR2zt2JPTxVwAyDX2PR1', timestamp=1704177900000.0, type='dca_executor', close_timestamp=1704189120000.0, close_type=, status=, config=DCAExecutorConfig(id='8TA9qPcksvZ9gtDM31BMdJiVsR2zt2JPTxVwAyDX2PR1', type='dca_executor', timestamp=1704177900000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380954'), Decimal('70.85714285714285714285714288'), Decimal('94.28571428571428571428571433'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.787010799999999972895395328'), Decimal('3.779436778399999972949604537'), Decimal('3.756714713599999973112232165'), Decimal('3.749140691999999973166441375'), Decimal('3.730205637999999973301964398'), Decimal('3.711270583999999973437487421')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0.036201224464641391931341019017054350115358829498291015625'), net_pnl_quote=Decimal('5.147469345305676569068964454345405101776123046875'), cum_fees_quote=Decimal('0.08531428571428571749901692555795307271182537078857421875'), filled_amount_quote=Decimal('142.19047619047620401033782400190830230712890625'), is_active=False, is_trading=False, custom_info={'close_price': 3.9043, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.769382049122572}, controller_id=None),\n ExecutorInfo(id='DkU1sUjB4rGrQtcnh3Y31oCKdrdH15MHaLT2LQUV2mDp', timestamp=1704189120000.0, type='dca_executor', close_timestamp=1704191940000.0, close_type=, status=, config=DCAExecutorConfig(id='DkU1sUjB4rGrQtcnh3Y31oCKdrdH15MHaLT2LQUV2mDp', type='dca_executor', timestamp=1704189120000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380952'), Decimal('70.85714285714285714285714290'), Decimal('94.28571428571428571428571428'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.896491400000000103369211801'), Decimal('3.888698417200000103162473377'), Decimal('3.865319468800000102542258107'), Decimal('3.857526486000000102335519683'), Decimal('3.838044029000000101818673624'), Decimal('3.818561572000000101301827565')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('-0.0132542149687274839198192211142668384127318859100341796875'), net_pnl_quote=Decimal('-6.54411085253575830478212083107791841030120849609375'), cum_fees_quote=Decimal('0.29624285714285714110616254401975311338901519775390625'), filled_amount_quote=Decimal('493.73809523809524080206756480038166046142578125'), is_active=False, is_trading=False, custom_info={'close_price': 3.7888, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.847848673898105}, controller_id=None),\n ExecutorInfo(id='HaKQSmrpfox47QNdakGadZjBSfeJ2mEAjiyNhHNqQYZK', timestamp=1704192600000.0, type='dca_executor', close_timestamp=1704192720000.0, close_type=, status=, config=DCAExecutorConfig(id='HaKQSmrpfox47QNdakGadZjBSfeJ2mEAjiyNhHNqQYZK', type='dca_executor', timestamp=1704192600000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428573'), Decimal('72.00000000000000000000000003'), Decimal('96.19047619047619047619047621'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.774533999999999904041717012'), Decimal('3.782083067999999903849800446'), Decimal('3.804730271999999903274050748'), Decimal('3.812279339999999903082134182'), Decimal('3.831152009999999902602342767'), Decimal('3.850024679999999902122551352')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.7523, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.774534}, controller_id=None),\n ExecutorInfo(id='BkfmvzjJWjFsjXjmPYb5Baa1k195rQ6EsL7yngZzHrca', timestamp=1704195540000.0, type='dca_executor', close_timestamp=1704195660000.0, close_type=, status=, config=DCAExecutorConfig(id='BkfmvzjJWjFsjXjmPYb5Baa1k195rQ6EsL7yngZzHrca', type='dca_executor', timestamp=1704195540000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380954'), Decimal('70.85714285714285714285714286'), Decimal('94.28571428571428571428571430'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.711661800000000072175569688'), Decimal('3.704238476400000072031218549'), Decimal('3.681968505600000071598165130'), Decimal('3.674545182000000071453813991'), Decimal('3.655986873000000071092936143'), Decimal('3.637428564000000070732058294')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.7206, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.7116618}, controller_id=None),\n ExecutorInfo(id='GhmdG1UuNYxHo5HjAVgMuktTav4nK3rZNWMYHAnsTcec', timestamp=1704195720000.0, type='dca_executor', close_timestamp=1704195840000.0, close_type=, status=, config=DCAExecutorConfig(id='GhmdG1UuNYxHo5HjAVgMuktTav4nK3rZNWMYHAnsTcec', type='dca_executor', timestamp=1704195720000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952380'), Decimal('47.52380952380952380952380950'), Decimal('70.85714285714285714285714287'), Decimal('94.28571428571428571428571428'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.712460199999999984244451745'), Decimal('3.705035279599999984275962842'), Decimal('3.682760518399999984370496131'), Decimal('3.675335597999999984402007228'), Decimal('3.656773296999999984480784969'), Decimal('3.638210995999999984559562710')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.7128, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.7124601999999998}, controller_id=None),\n ExecutorInfo(id='7QufBAMdvc1eeup3wNAH2aDKjJq2w4MiaYecsNxBAUFJ', timestamp=1704195900000.0, type='dca_executor', close_timestamp=1704202320000.0, close_type=, status=, config=DCAExecutorConfig(id='7QufBAMdvc1eeup3wNAH2aDKjJq2w4MiaYecsNxBAUFJ', type='dca_executor', timestamp=1704195900000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380955'), Decimal('70.85714285714285714285714289'), Decimal('94.28571428571428571428571430'), Decimal('117.2619047619047619047619048'), Decimal('140.0000000000000000000000000')], prices=[Decimal('3.719146799999999912622886122'), Decimal('3.711708506399999912797640350'), Decimal('3.689393625599999913321903033'), Decimal('3.681955331999999913496657261'), Decimal('3.663359597999999913933542830'), Decimal('3.644763863999999914370428400')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('0.0322810098878496543051141998148523271083831787109375'), net_pnl_quote=Decimal('2.302712038666609117854022770188748836517333984375'), cum_fees_quote=Decimal('0.042800000000000004707345624410663731396198272705078125'), filled_amount_quote=Decimal('71.333333333333342807236476801335811614990234375'), is_active=False, is_trading=False, custom_info={'close_price': 3.832, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.714191247922029}, controller_id=None),\n ExecutorInfo(id='6LkhuSxFWhpFFSmyFSkMFr2EdcADe7Z9CKfnxLEFedeN', timestamp=1704202320000.0, type='dca_executor', close_timestamp=1704205500000.0, close_type=, status=, config=DCAExecutorConfig(id='6LkhuSxFWhpFFSmyFSkMFr2EdcADe7Z9CKfnxLEFedeN', type='dca_executor', timestamp=1704202320000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.52380952380952380952380950'), Decimal('70.85714285714285714285714287'), Decimal('94.28571428571428571428571429'), Decimal('117.2619047619047619047619048'), Decimal('139.9999999999999999999999999')], prices=[Decimal('3.824335999999999850924914391'), Decimal('3.816687327999999851223064562'), Decimal('3.793741311999999852117515076'), Decimal('3.786092639999999852415665247'), Decimal('3.766970959999999853161040675'), Decimal('3.747849279999999853906416103')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='buy_0'), net_pnl_pct=Decimal('-0.01559459184714712988439888619041084893979132175445556640625'), net_pnl_quote=Decimal('-7.69964407462595357145573871093802154064178466796875'), cum_fees_quote=Decimal('0.29624285714285714110616254401975311338901519775390625'), filled_amount_quote=Decimal('493.73809523809524080206756480038166046142578125'), is_active=False, is_trading=False, custom_info={'close_price': 3.714, 'level_id': 'buy_0', 'side': , 'current_position_average_price': 3.776594042050441}, controller_id=None),\n ExecutorInfo(id='346HDSG85aqivXjSABkFJSoQPYW4DgT9iNyh2uG6ufhg', timestamp=1704192780000.0, type='dca_executor', close_timestamp=1704205920000.0, close_type=, status=, config=DCAExecutorConfig(id='346HDSG85aqivXjSABkFJSoQPYW4DgT9iNyh2uG6ufhg', type='dca_executor', timestamp=1704192780000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952382'), Decimal('47.71428571428571428571428574'), Decimal('72.00000000000000000000000005'), Decimal('96.19047619047619047619047624'), Decimal('120.8333333333333333333333334'), Decimal('145.7142857142857142857142858')], prices=[Decimal('3.760506000000000114070461408'), Decimal('3.768027012000000114298602331'), Decimal('3.790590048000000114983025099'), Decimal('3.798111060000000115211166022'), Decimal('3.816913590000000115781518329'), Decimal('3.835716120000000116351870636')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0.034961247657317638670715354010098963044583797454833984375'), net_pnl_quote=Decimal('17.69954783184630997538988594897091388702392578125'), cum_fees_quote=Decimal('0.3037571428571428366893769634771160781383514404296875'), filled_amount_quote=Decimal('506.26190476190475919793243519961833953857421875'), is_active=False, is_trading=False, custom_info={'close_price': 3.6854, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.8077488709612943}, controller_id=None),\n ExecutorInfo(id='GGSePg14t2FWjCC8VxyibfyCR2MLkmg6w7ATy6byG5Ju', timestamp=1704205920000.0, type='dca_executor', close_timestamp=1704206040000.0, close_type=, status=, config=DCAExecutorConfig(id='GGSePg14t2FWjCC8VxyibfyCR2MLkmg6w7ATy6byG5Ju', type='dca_executor', timestamp=1704205920000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428570'), Decimal('72.00000000000000000000000001'), Decimal('96.19047619047619047619047620'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.692770800000000009408965272'), Decimal('3.700156341600000009427783203'), Decimal('3.722312966400000009484236994'), Decimal('3.729698508000000009503054925'), Decimal('3.748162362000000009550099751'), Decimal('3.766626216000000009597144577')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.6769, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6927708}, controller_id=None),\n ExecutorInfo(id='7rAyQqPqub9DsQLHYdGFaxhfEzyxJkfj4AR6mQ97Vr8b', timestamp=1704206100000.0, type='dca_executor', close_timestamp=1704211860000.0, close_type=, status=, config=DCAExecutorConfig(id='7rAyQqPqub9DsQLHYdGFaxhfEzyxJkfj4AR6mQ97Vr8b', type='dca_executor', timestamp=1704206100000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428572'), Decimal('72.00000000000000000000000002'), Decimal('96.19047619047619047619047619'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.677239800000000162480541520'), Decimal('3.684594279600000162805502603'), Decimal('3.706657718400000163780385852'), Decimal('3.714012198000000164105346935'), Decimal('3.732398397000000164917749643'), Decimal('3.750784596000000165730152350')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0.031711147218162601124635813221175340004265308380126953125'), net_pnl_quote=Decimal('2.268102053413343810461810790002346038818359375'), cum_fees_quote=Decimal('0.0429142857142857103713851074644480831921100616455078125'), filled_amount_quote=Decimal('71.5238095238095183958648703992366790771484375'), is_active=False, is_trading=False, custom_info={'close_price': 3.5685, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.6821460507051933}, controller_id=None),\n ExecutorInfo(id='BKuDzKDZk3LCbwXaoiLoMhvgDygUQYSRMg5rqQz6smDi', timestamp=1704211860000.0, type='dca_executor', close_timestamp=1704211980000.0, close_type=, status=, config=DCAExecutorConfig(id='BKuDzKDZk3LCbwXaoiLoMhvgDygUQYSRMg5rqQz6smDi', type='dca_executor', timestamp=1704211860000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952382'), Decimal('47.71428571428571428571428571'), Decimal('72.00000000000000000000000003'), Decimal('96.19047619047619047619047624'), Decimal('120.8333333333333333333333334'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.575636999999999782999603180'), Decimal('3.582788273999999782565602386'), Decimal('3.604242095999999781263600005'), Decimal('3.611393369999999780829599212'), Decimal('3.629271554999999779744597228'), Decimal('3.647149739999999778659595244')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.5699, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.575637}, controller_id=None),\n ExecutorInfo(id='7W2XaaDr9XMtareJnGAcyZGiac9T1UsxV9eACkV7uvjv', timestamp=1704212040000.0, type='dca_executor', close_timestamp=1704212160000.0, close_type=, status=, config=DCAExecutorConfig(id='7W2XaaDr9XMtareJnGAcyZGiac9T1UsxV9eACkV7uvjv', type='dca_executor', timestamp=1704212040000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952380'), Decimal('47.71428571428571428571428569'), Decimal('72.00000000000000000000000000'), Decimal('96.19047619047619047619047618'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142856')], prices=[Decimal('3.578442599999999918984809609'), Decimal('3.585599485199999918822779228'), Decimal('3.607070140799999918336688086'), Decimal('3.614227025999999918174657705'), Decimal('3.632119238999999917769581753'), Decimal('3.650011451999999917364505801')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.5759, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.5784426}, controller_id=None),\n ExecutorInfo(id='3WWGshTprU6CHY7gNha3QSU7DSKbvfFRZwZ5uwLUcVxt', timestamp=1704212220000.0, type='dca_executor', close_timestamp=1704212340000.0, close_type=, status=, config=DCAExecutorConfig(id='3WWGshTprU6CHY7gNha3QSU7DSKbvfFRZwZ5uwLUcVxt', type='dca_executor', timestamp=1704212220000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952382'), Decimal('47.71428571428571428571428572'), Decimal('72.00000000000000000000000004'), Decimal('96.19047619047619047619047624'), Decimal('120.8333333333333333333333334'), Decimal('145.7142857142857142857142858')], prices=[Decimal('3.587460599999999815750430231'), Decimal('3.594635521199999815381931091'), Decimal('3.616160284799999814276433673'), Decimal('3.623335205999999813907934533'), Decimal('3.641272508999999812986686684'), Decimal('3.659209811999999812065438836')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.5704, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.5874606}, controller_id=None),\n ExecutorInfo(id='3V4eLchsWfVTJhkHE192wP9CNom8NzsYQYNWPpunetsC', timestamp=1704212400000.0, type='dca_executor', close_timestamp=1704212520000.0, close_type=, status=, config=DCAExecutorConfig(id='3V4eLchsWfVTJhkHE192wP9CNom8NzsYQYNWPpunetsC', type='dca_executor', timestamp=1704212400000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952380'), Decimal('47.71428571428571428571428570'), Decimal('72.00000000000000000000000002'), Decimal('96.19047619047619047619047618'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.580045800000000187395236827'), Decimal('3.587205891600000187770027301'), Decimal('3.608686166400000188894398722'), Decimal('3.615846258000000189269189195'), Decimal('3.633746487000000190206165379'), Decimal('3.651646716000000191143141564')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.5572, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.5800458}, controller_id=None),\n ExecutorInfo(id='Fz2F9cR4yjRAp94MJ2Py9p9cJodsg99Xc5GkDXs94dCm', timestamp=1704212580000.0, type='dca_executor', close_timestamp=1704212700000.0, close_type=, status=, config=DCAExecutorConfig(id='Fz2F9cR4yjRAp94MJ2Py9p9cJodsg99Xc5GkDXs94dCm', type='dca_executor', timestamp=1704212580000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952381'), Decimal('47.71428571428571428571428571'), Decimal('72.00000000000000000000000000'), Decimal('96.19047619047619047619047618'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.575136000000000060665472644'), Decimal('3.582286272000000060786803589'), Decimal('3.603737088000000061150796425'), Decimal('3.610887360000000061272127370'), Decimal('3.628763040000000061575454734'), Decimal('3.646638720000000061878782097')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0'), net_pnl_quote=Decimal('0'), cum_fees_quote=Decimal('0'), filled_amount_quote=Decimal('0'), is_active=False, is_trading=False, custom_info={'close_price': 3.5744, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.575136}, controller_id=None),\n ExecutorInfo(id='6zxuKQfrSfHxjHinTQVQQtuzU4p45Ct52MQAh9tdJeAE', timestamp=1704212760000.0, type='dca_executor', close_timestamp=1704229380000.0, close_type=, status=, config=DCAExecutorConfig(id='6zxuKQfrSfHxjHinTQVQQtuzU4p45Ct52MQAh9tdJeAE', type='dca_executor', timestamp=1704212760000.0, controller_id='main', connector_name='kucoin', trading_pair='WLD-USDT', side=, leverage=20, amounts_quote=[Decimal('23.80952380952380952380952380'), Decimal('47.71428571428571428571428571'), Decimal('72.00000000000000000000000000'), Decimal('96.19047619047619047619047619'), Decimal('120.8333333333333333333333333'), Decimal('145.7142857142857142857142857')], prices=[Decimal('3.590767200000000119047155465'), Decimal('3.597948734400000119285249776'), Decimal('3.619493337600000119999532709'), Decimal('3.626674872000000120237627020'), Decimal('3.644628708000000120832862797'), Decimal('3.662582544000000121428098574')], take_profit=Decimal('0.03'), stop_loss=Decimal('0.015'), trailing_stop=TrailingStop(activation_price=Decimal('0.008'), trailing_delta=Decimal('0.004')), time_limit=43200, mode=, activation_bounds=[Decimal('0.01')], level_id='sell_0'), net_pnl_pct=Decimal('0.03109490402272947762174482022601296193897724151611328125'), net_pnl_quote=Decimal('11.21119360990934410438057966530323028564453125'), cum_fees_quote=Decimal('0.2163285714285713978721759076506714336574077606201171875'), filled_amount_quote=Decimal('360.547619047619036791729740798473358154296875'), is_active=False, is_trading=False, custom_info={'close_price': 3.515, 'level_id': 'sell_0', 'side': , 'current_position_average_price': 3.625084956258337}, controller_id=None),\n ExecutorInfo(id='HL62x9eQoubPuYEARCrrMmQSa4VGMdxXZttCmbQFvHrP', timestamp=1704229380000.0, type='dca_executor', close_timestamp=1704240600000.0, close_type=, status=