diff --git a/main.py b/main.py index 9a9536b1..98b2e6fc 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ import streamlit as st -from st_pages import Page, Section, show_pages, add_page_title +from st_pages import Page, Section, show_pages from utils.st_utils import initialize_st_page @@ -8,12 +8,16 @@ show_pages( [ Page("main.py", "Hummingbot Dashboard", "πŸ“Š"), - Section("Foundation Pages", "🏠"), - Page("pages/bot_orchestration/app.py", "Bot Orchestration", "πŸ™"), + Section("Bot Orchestration", "πŸ™"), + Page("pages/bot_orchestration/app.py", "Bots Manager", "πŸ¦…"), Page("pages/file_manager/app.py", "File Manager", "πŸ—‚"), + Section("Backtest Manager", "βš™οΈ"), + Page("pages/backtest_manager/create.py", "Create", "βš”οΈ"), + Page("pages/backtest_manager/optimize.py", "Optimize", "πŸ§ͺ"), + Page("pages/backtest_manager/analyze.py", "Analyze", "πŸ”¬"), + Page("pages/backtest_manager/simulate.py", "Simulate", "πŸ“ˆ"), Section("Community Pages", "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦"), Page("pages/strategy_performance/app.py", "Strategy Performance", "πŸš€"), - Page("pages/backtest_manager/app.py", "Backtest Manager", "βš™οΈ"), Page("pages/candles_downloader/app.py", "Candles Downloader", "πŸ—‚"), Page("pages/db_inspector/app.py", "DB Inspector", "πŸ”"), Page("pages/token_spreads/app.py", "Token Spreads", "πŸ§™"), diff --git a/pages/backtest_manager/analyze.py b/pages/backtest_manager/analyze.py new file mode 100644 index 00000000..c0dc2095 --- /dev/null +++ b/pages/backtest_manager/analyze.py @@ -0,0 +1,4 @@ +from utils.st_utils import initialize_st_page + + +initialize_st_page(title="Analyze", icon="πŸ”¬", initial_sidebar_state="collapsed") diff --git a/pages/backtest_manager/app.py b/pages/backtest_manager/app.py deleted file mode 100644 index 583e0d6d..00000000 --- a/pages/backtest_manager/app.py +++ /dev/null @@ -1,173 +0,0 @@ -import datetime -import threading -import webbrowser - -import streamlit as st -from streamlit_ace import st_ace - -import constants -from quants_lab.strategy.strategy_analysis import StrategyAnalysis -from utils import os_utils -from utils.file_templates import strategy_optimization_template, directional_strategy_template -from utils.os_utils import load_directional_strategies, save_file, get_function_from_file -import optuna - -from utils.st_utils import initialize_st_page - - -initialize_st_page(title="Backtest Manager", icon="βš™οΈ") - -# Start content here -if "strategy_params" not in st.session_state: - st.session_state.strategy_params = {} - -create, modify, backtest, optimize, analyze = st.tabs(["Create", "Modify", "Backtest", "Optimize", "Analyze"]) - -with create: - # TODO: - # * Add videos explaining how to the triple barrier method works and how the backtesting is designed, - # link to video of how to create a strategy, etc in a toggle. - # * Add functionality to start strategy creation from scratch or by duplicating an existing one - c1, c2 = st.columns([4, 1]) - with c1: - # TODO: Allow change the strategy name and see the effect in the code - strategy_name = st.text_input("Strategy class name", value="CustomStrategy") - with c2: - update_strategy_name = st.button("Update Strategy Name") - - c1, c2 = st.columns([4, 1]) - with c1: - # TODO: every time that we save and run the optimizations, we should save the code in a file - # so the user then can correlate the results with the code. - if update_strategy_name: - st.session_state.directional_strategy_code = st_ace(key="create_directional_strategy", - value=directional_strategy_template(strategy_name), - language='python', - keybinding='vscode', - theme='pastel_on_dark') - with c2: - if st.button("Save strategy"): - save_file(name=f"{strategy_name.lower()}.py", content=st.session_state.directional_strategy_code, - path=constants.DIRECTIONAL_STRATEGIES_PATH) - st.success(f"Strategy {strategy_name} saved successfully") - -with modify: - pass - -with backtest: - # TODO: - # * Add videos explaining how to the triple barrier method works and how the backtesting is designed, - # link to video of how to create a strategy, etc in a toggle. - # * Add performance analysis graphs of the backtesting run - strategies = load_directional_strategies(constants.DIRECTIONAL_STRATEGIES_PATH) - strategy_to_optimize = st.selectbox("Select strategy to backtest", strategies.keys()) - strategy = strategies[strategy_to_optimize] - strategy_config = strategy["config"] - field_schema = strategy_config.schema()["properties"] - st.write("## Strategy parameters") - c1, c2 = st.columns([5, 1]) - with c1: - columns = st.columns(4) - column_index = 0 - for field_name, properties in field_schema.items(): - field_type = properties["type"] - with columns[column_index]: - if field_type in ["number", "integer"]: - field_value = st.number_input(field_name, - value=properties["default"], - min_value=properties.get("minimum"), - max_value=properties.get("maximum"), - key=field_name) - elif field_type == "string": - field_value = st.text_input(field_name, value=properties["default"]) - elif field_type == "boolean": - # TODO: Add support for boolean fields in optimize tab - field_value = st.checkbox(field_name, value=properties["default"]) - else: - raise ValueError(f"Field type {field_type} not supported") - st.session_state["strategy_params"][field_name] = field_value - column_index = (column_index + 1) % 4 - with c2: - add_positions = st.checkbox("Add positions", value=True) - add_volume = st.checkbox("Add volume", value=True) - add_pnl = st.checkbox("Add PnL", value=True) - - run_backtesting_button = st.button("Run Backtesting!") - if run_backtesting_button: - config = strategy["config"](**st.session_state["strategy_params"]) - strategy = strategy["class"](config=config) - # TODO: add form for order amount, leverage, tp, sl, etc. - - market_data, positions = strategy.run_backtesting( - start='2021-04-01', - order_amount=50, - leverage=20, - initial_portfolio=100, - take_profit_multiplier=2.3, - stop_loss_multiplier=1.2, - time_limit=60 * 60 * 3, - std_span=None, - ) - strategy_analysis = StrategyAnalysis( - positions=positions, - candles_df=market_data, - ) - st.text(strategy_analysis.text_report()) - # TODO: check why the pnl is not being plotted - strategy_analysis.create_base_figure(volume=add_volume, positions=add_positions, trade_pnl=add_pnl) - st.plotly_chart(strategy_analysis.figure(), use_container_width=True) - -with optimize: - # TODO: - # * Add videos explaining how to use the optimization tool, quick intro to optuna, etc in a toggle - with st.container(): - c1, c2, c3 = st.columns([1, 1, 1]) - with c1: - strategies = load_directional_strategies(constants.DIRECTIONAL_STRATEGIES_PATH) - strategy_to_optimize = st.selectbox("Select strategy to optimize", strategies.keys()) - with c2: - today = datetime.datetime.today() - # TODO: add hints about the study name - STUDY_NAME = st.text_input("Study name", - f"{strategy_to_optimize}_study_{today.day:02d}-{today.month:02d}-{today.year}") - with c3: - generate_optimization_code_button = st.button("Generate Optimization Code") - if st.button("Launch optuna dashboard"): - os_utils.execute_bash_command(f"optuna-dashboard sqlite:///data/backtesting/backtesting_report.db") - webbrowser.open("http://127.0.0.1:8080/dashboard", new=2) - - c1, c2 = st.columns([4, 1]) - if generate_optimization_code_button: - with c1: - # TODO: every time that we save and run the optimizations, we should save the code in a file - # so the user then can correlate the results with the code. - st.session_state.optimization_code = st_ace(key="create_optimization_code", - value=strategy_optimization_template( - strategy_info=strategies[strategy_to_optimize]), - language='python', - keybinding='vscode', - theme='pastel_on_dark') - if "optimization_code" in st.session_state: - with c2: - if st.button("Run optimization"): - save_file(name=f"{STUDY_NAME}.py", content=st.session_state.optimization_code, path=constants.OPTIMIZATIONS_PATH) - study = optuna.create_study(direction="maximize", study_name=STUDY_NAME, - storage="sqlite:///data/backtesting/backtesting_report.db", - load_if_exists=True) - objective = get_function_from_file(file_path=f"{constants.OPTIMIZATIONS_PATH}/{STUDY_NAME}.py", - function_name="objective") - - - def optimization_process(): - study.optimize(objective, n_trials=2000) - - - optimization_thread = threading.Thread(target=optimization_process) - optimization_thread.start() - - -with analyze: - # TODO: - # * Add graphs for all backtesting results - # * Add management of backtesting results (delete, rename, save, share, upload s3, etc) - pass diff --git a/pages/backtest_manager/create.py b/pages/backtest_manager/create.py new file mode 100644 index 00000000..2b0c7d9e --- /dev/null +++ b/pages/backtest_manager/create.py @@ -0,0 +1,49 @@ +from types import SimpleNamespace + +import streamlit as st +from streamlit_elements import elements, mui + +from ui_components.dashboard import Dashboard +from ui_components.directional_strategies_file_explorer import DirectionalStrategiesFileExplorer +from ui_components.directional_strategy_creation_card import DirectionalStrategyCreationCard +from ui_components.editor import Editor + +from utils.st_utils import initialize_st_page + + +initialize_st_page(title="Create", icon="οΈβš”οΈ", initial_sidebar_state="collapsed") + +# TODO: +# * Add videos explaining how to the triple barrier method works and how the backtesting is designed, +# link to video of how to create a strategy, etc in a toggle. +# * Add functionality to start strategy creation from scratch or by duplicating an existing one + +if "ds_board" not in st.session_state: + board = Dashboard() + ds_board = SimpleNamespace( + dashboard=board, + create_strategy_card=DirectionalStrategyCreationCard(board, 0, 0, 12, 1), + file_explorer=DirectionalStrategiesFileExplorer(board, 0, 2, 3, 7), + editor=Editor(board, 4, 2, 9, 7), + ) + st.session_state.ds_board = ds_board + +else: + ds_board = st.session_state.ds_board + +# Add new tabs +for tab_name, content in ds_board.file_explorer.tabs.items(): + if tab_name not in ds_board.editor.tabs: + ds_board.editor.add_tab(tab_name, content["content"], content["language"]) + +# Remove deleted tabs +for tab_name in list(ds_board.editor.tabs.keys()): + if tab_name not in ds_board.file_explorer.tabs: + ds_board.editor.remove_tab(tab_name) + +with elements("directional_strategies"): + with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True): + with ds_board.dashboard(): + ds_board.create_strategy_card() + ds_board.file_explorer() + ds_board.editor() diff --git a/pages/backtest_manager/optimize.py b/pages/backtest_manager/optimize.py new file mode 100644 index 00000000..1c7f4965 --- /dev/null +++ b/pages/backtest_manager/optimize.py @@ -0,0 +1,68 @@ +import time +import webbrowser +from types import SimpleNamespace + +import streamlit as st +from streamlit_elements import elements, mui + +import constants +from quants_lab.strategy.strategy_analysis import StrategyAnalysis +from ui_components.dashboard import Dashboard +from ui_components.directional_strategies_file_explorer import DirectionalStrategiesFileExplorer +from ui_components.directional_strategy_creation_card import DirectionalStrategyCreationCard +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 utils.os_utils import load_directional_strategies + +from utils.st_utils import initialize_st_page + +initialize_st_page(title="Optimize", icon="πŸ§ͺ", initial_sidebar_state="collapsed") + + +def run_optuna_dashboard(): + os_utils.execute_bash_command(f"optuna-dashboard sqlite:///data/backtesting/backtesting_report.db") + time.sleep(5) + webbrowser.open("http://127.0.0.1:8080/dashboard", new=2) + + +if "op_board" not in st.session_state: + board = Dashboard() + op_board = SimpleNamespace( + dashboard=board, + create_optimization_card=OptimizationCreationCard(board, 0, 0, 6, 1), + run_optimization_card=OptimizationRunCard(board, 6, 0, 6, 1), + file_explorer=OptimizationsStrategiesFileExplorer(board, 0, 2, 3, 7), + editor=Editor(board, 4, 2, 9, 7), + ) + st.session_state.op_board = op_board + +else: + op_board = st.session_state.op_board + +# Add new tabs +for tab_name, content in op_board.file_explorer.tabs.items(): + if tab_name not in op_board.editor.tabs: + op_board.editor.add_tab(tab_name, content["content"], content["language"]) + +# Remove deleted tabs +for tab_name in list(op_board.editor.tabs.keys()): + if tab_name not in op_board.file_explorer.tabs: + op_board.editor.remove_tab(tab_name) + +with elements("optimizations"): + with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True): + with mui.Grid(container=True, spacing=2): + with mui.Grid(item=True, xs=10): + pass + with mui.Grid(item=True, xs=2): + with mui.Fab(variant="extended", color="primary", size="large", onClick=run_optuna_dashboard): + mui.Typography("Open Optuna Dashboard", variant="body1") + + with op_board.dashboard(): + op_board.create_optimization_card() + op_board.run_optimization_card() + op_board.file_explorer() + op_board.editor() diff --git a/pages/backtest_manager/simulate.py b/pages/backtest_manager/simulate.py new file mode 100644 index 00000000..260cbf5b --- /dev/null +++ b/pages/backtest_manager/simulate.py @@ -0,0 +1,90 @@ +import time +import webbrowser +from types import SimpleNamespace + +import streamlit as st +from streamlit_elements import elements, mui + +import constants +from quants_lab.strategy.strategy_analysis import StrategyAnalysis +from ui_components.dashboard import Dashboard +from ui_components.directional_strategies_file_explorer import DirectionalStrategiesFileExplorer +from ui_components.directional_strategy_creation_card import DirectionalStrategyCreationCard +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 utils.os_utils import load_directional_strategies + +from utils.st_utils import initialize_st_page + + +initialize_st_page(title="Simulate", icon="πŸ“ˆ", initial_sidebar_state="collapsed") + +# Start content here +if "strategy_params" not in st.session_state: + st.session_state.strategy_params = {} + + +# TODO: +# * Add videos explaining how to the triple barrier method works and how the backtesting is designed, +# link to video of how to create a strategy, etc in a toggle. +# * Add performance analysis graphs of the backtesting run +strategies = load_directional_strategies(constants.DIRECTIONAL_STRATEGIES_PATH) +strategy_to_optimize = st.selectbox("Select strategy to backtest", strategies.keys()) +strategy = strategies[strategy_to_optimize] +strategy_config = strategy["config"] +field_schema = strategy_config.schema()["properties"] +st.write("## Strategy parameters") +c1, c2 = st.columns([5, 1]) +with c1: + columns = st.columns(4) + column_index = 0 + for field_name, properties in field_schema.items(): + field_type = properties["type"] + with columns[column_index]: + if field_type in ["number", "integer"]: + field_value = st.number_input(field_name, + value=properties["default"], + min_value=properties.get("minimum"), + max_value=properties.get("maximum"), + key=field_name) + elif field_type == "string": + field_value = st.text_input(field_name, value=properties["default"]) + elif field_type == "boolean": + # TODO: Add support for boolean fields in optimize tab + field_value = st.checkbox(field_name, value=properties["default"]) + else: + raise ValueError(f"Field type {field_type} not supported") + st.session_state["strategy_params"][field_name] = field_value + column_index = (column_index + 1) % 4 +with c2: + add_positions = st.checkbox("Add positions", value=True) + add_volume = st.checkbox("Add volume", value=True) + add_pnl = st.checkbox("Add PnL", value=True) + + run_backtesting_button = st.button("Run Backtesting!") +if run_backtesting_button: + config = strategy["config"](**st.session_state["strategy_params"]) + strategy = strategy["class"](config=config) + # TODO: add form for order amount, leverage, tp, sl, etc. + + market_data, positions = strategy.run_backtesting( + start='2021-04-01', + order_amount=50, + leverage=20, + initial_portfolio=100, + take_profit_multiplier=2.3, + stop_loss_multiplier=1.2, + time_limit=60 * 60 * 3, + std_span=None, + ) + strategy_analysis = StrategyAnalysis( + positions=positions, + candles_df=market_data, + ) + st.text(strategy_analysis.text_report()) + # TODO: check why the pnl is not being plotted + strategy_analysis.create_base_figure(volume=add_volume, positions=add_positions, trade_pnl=add_pnl) + st.plotly_chart(strategy_analysis.figure(), use_container_width=True) diff --git a/pages/bot_orchestration/app.py b/pages/bot_orchestration/app.py index f40a09f3..b67f6469 100644 --- a/pages/bot_orchestration/app.py +++ b/pages/bot_orchestration/app.py @@ -1,4 +1,3 @@ -from glob import glob from types import SimpleNamespace from commlib.exceptions import RPCClientTimeoutError @@ -33,6 +32,10 @@ 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 manage_broker_container(): if st.session_state.is_broker_running: docker_manager.stop_container("hummingbot-broker") diff --git a/pages/file_manager/app.py b/pages/file_manager/app.py index 4843ca8a..56c81806 100644 --- a/pages/file_manager/app.py +++ b/pages/file_manager/app.py @@ -1,44 +1,40 @@ -import streamlit as st -from streamlit_elements import elements, mui, lazy, sync, event - from types import SimpleNamespace - import streamlit as st -from streamlit_elements import elements, mui, lazy, sync, event +from streamlit_elements import elements, mui +from ui_components.bots_file_explorer import BotsFileExplorer from ui_components.dashboard import Dashboard -from ui_components.file_explorer import FileExplorer from ui_components.editor import Editor from utils.st_utils import initialize_st_page -initialize_st_page(title="File Manager", icon="πŸ—‚οΈ", initial_sidebar_state="expanded") -if "editor_tabs" not in st.session_state: - st.session_state.editor_tabs = {} +initialize_st_page(title="File Manager", icon="πŸ—‚οΈ", initial_sidebar_state="collapsed") -# TODO: Use this to save the current file selected -if "selected_file" not in st.session_state: - st.session_state.selected_file = "" -if "w" not in st.session_state: +if "fe_board" not in st.session_state: board = Dashboard() - w = SimpleNamespace( + fe_board = SimpleNamespace( dashboard=board, - file_explorer=FileExplorer(board, 0, 0, 3, 7), + file_explorer=BotsFileExplorer(board, 0, 0, 3, 7), editor=Editor(board, 4, 0, 9, 7), ) - st.session_state.w = w + st.session_state.fe_board = fe_board + else: - w = st.session_state.w - -for tab_name, content in st.session_state.editor_tabs.items(): - if tab_name not in w.editor._tabs: - w.editor.add_tab(tab_name, content["content"], content["language"], content["file_path"]) - -with elements("bot_config"): - with mui.Paper(style={"padding": "2rem"}, variant="outlined"): - mui.Typography("Select a file and click ✏️ to edit it or πŸ—‘οΈ to delete it. Click πŸ’Ύ to save your changes.", variant="body1", sx={"margin-bottom": "2rem"}) - event.Hotkey("ctrl+s", sync(), bindInputs=True, overrideDefault=True) - with w.dashboard(): - w.file_explorer() - w.editor() + fe_board = st.session_state.fe_board + +# Add new tabs +for tab_name, content in fe_board.file_explorer.tabs.items(): + if tab_name not in fe_board.editor.tabs: + fe_board.editor.add_tab(tab_name, content["content"], content["language"]) + +# Remove deleted tabs +for tab_name in list(fe_board.editor.tabs.keys()): + if tab_name not in fe_board.file_explorer.tabs: + fe_board.editor.remove_tab(tab_name) + +with elements("file_manager"): + with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True): + with fe_board.dashboard(): + fe_board.file_explorer() + fe_board.editor() diff --git a/ui_components/bots_file_explorer.py b/ui_components/bots_file_explorer.py new file mode 100644 index 00000000..6b9da2ec --- /dev/null +++ b/ui_components/bots_file_explorer.py @@ -0,0 +1,26 @@ +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, \ + get_yml_files_from_directory, get_log_files_from_directory + + +class BotsFileExplorer(FileExplorerBase): + def add_tree_view(self): + directory = constants.BOTS_FOLDER + bots = [bot.split("/")[-2] for bot in get_directories_from_directory(directory) if + "data_downloader" not in bot] + with mui.lab.TreeView(defaultExpandIcon=mui.icon.ChevronRight, defaultCollapseIcon=mui.icon.ExpandMore, + onNodeSelect=lambda event, node_id: self.set_selected_file(event, node_id)): + for bot in bots: + with mui.lab.TreeItem(nodeId=bot, label=f"πŸ€–{bot}"): + with mui.lab.TreeItem(nodeId=f"scripts_{bot}", label="🐍Scripts"): + for file in get_python_files_from_directory(f"{directory}/{bot}/scripts"): + mui.lab.TreeItem(nodeId=file, label=f"πŸ“„{file.split('/')[-1]}") + with mui.lab.TreeItem(nodeId=f"strategies_{bot}", label="πŸ“œStrategies"): + for file in get_yml_files_from_directory(f"{directory}/{bot}/conf/strategies"): + mui.lab.TreeItem(nodeId=file, label=f"πŸ“„ {file.split('/')[-1]}") + with mui.lab.TreeItem(nodeId=f"logs_{bot}", label="πŸ—„οΈLogs"): + for file in get_log_files_from_directory(f"{directory}/{bot}/logs"): + mui.lab.TreeItem(nodeId=file, label=f"πŸ“„ {file.split('/')[-1]}") diff --git a/ui_components/directional_strategies_file_explorer.py b/ui_components/directional_strategies_file_explorer.py new file mode 100644 index 00000000..80739724 --- /dev/null +++ b/ui_components/directional_strategies_file_explorer.py @@ -0,0 +1,16 @@ +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 + + +class DirectionalStrategiesFileExplorer(FileExplorerBase): + def add_tree_view(self): + with mui.lab.TreeView(defaultExpandIcon=mui.icon.ChevronRight, defaultCollapseIcon=mui.icon.ExpandMore, + onNodeSelect=lambda event, node_id: self.set_selected_file(event, node_id), + defaultExpanded=["directional_strategies"]): + with mui.lab.TreeItem(nodeId="directional_strategies", label=f"βš”οΈDirectional Strategies"): + strategies = get_python_files_from_directory(constants.DIRECTIONAL_STRATEGIES_PATH) + for strategy in strategies: + mui.lab.TreeItem(nodeId=strategy, label=f"🐍{strategy.split('/')[-1]}") diff --git a/ui_components/directional_strategy_creation_card.py b/ui_components/directional_strategy_creation_card.py new file mode 100644 index 00000000..cc42777e --- /dev/null +++ b/ui_components/directional_strategy_creation_card.py @@ -0,0 +1,47 @@ +from streamlit_elements import mui, lazy + +import constants +from utils.file_templates import directional_strategy_template +from utils.os_utils import save_file +from .dashboard import Dashboard + + +class DirectionalStrategyCreationCard(Dashboard.Item): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._strategy_name = "CustomStrategy" + self._strategy_type = "directional" + + def _set_strategy_name(self, event): + self._strategy_name = event.target.value + + def _set_strategy_type(self, _, childs): + self._strategy_type = childs.props.value + + def _create_strategy(self): + if self._strategy_type == "directional": + strategy_code = directional_strategy_template(self._strategy_name) + save_file(name=f"{self._strategy_name.lower()}.py", content=strategy_code, + path=constants.DIRECTIONAL_STRATEGIES_PATH) + + def __call__(self): + with mui.Paper(key=self._key, sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, elevation=1): + with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False): + mui.icon.NoteAdd() + mui.Typography("Create new strategy", variant="h6") + + with mui.Grid(container=True, spacing=2, sx={"padding": "10px"}): + with mui.Grid(item=True, xs=5): + with mui.FormControl(variant="standard", sx={"width": "100%"}): + mui.FormHelperText("Template name") + with mui.Select(label="Select strategy type", defaultValue="directional", + variant="standard", onChange=lazy(self._set_strategy_type)): + mui.MenuItem("Directional", value="directional") + with mui.Grid(item=True, xs=5): + with mui.FormControl(variant="standard", sx={"width": "100%"}): + mui.TextField(defaultValue="CustomStrategy", label="Strategy Name", + variant="standard", onChange=lazy(self._set_strategy_name)) + with mui.Grid(item=True, xs=2): + with mui.Button(variant="contained", onClick=self._create_strategy): + mui.icon.Add() + mui.Typography("Create", variant="body1") diff --git a/ui_components/editor.py b/ui_components/editor.py index 6ba14a1d..5e825ede 100644 --- a/ui_components/editor.py +++ b/ui_components/editor.py @@ -1,6 +1,6 @@ from functools import partial import streamlit as st -from streamlit_elements import mui, editor, sync, lazy +from streamlit_elements import mui, editor, sync, lazy, event from utils.os_utils import save_file from .dashboard import Dashboard @@ -26,31 +26,33 @@ def save_file(self): if len(self._tabs) > 0: label = list(self._tabs.keys())[self._index] content = self.get_content(label) - full_path = self.get_file_path(label) - file_name = full_path.split("/")[-1] - path = "/".join(full_path.split("/")[:-1]) + file_name = label.split("/")[-1] + path = "/".join(label.split("/")[:-1]) save_file(name=file_name, content=content, path=path) st.info("File saved") def _change_tab(self, _, index): self._index = index + @property + def tabs(self): + return self._tabs + def update_content(self, label, content): self._tabs[label]["content"] = content - def add_tab(self, label, default_content, language, file_path): + def add_tab(self, label, default_content, language): self._tabs[label] = { "content": default_content, "language": language, - "file_path": file_path } + def remove_tab(self, label): + del self._tabs[label] + def get_content(self, label): return self._tabs[label]["content"] - def get_file_path(self, label): - return self._tabs[label]["file_path"] - def __call__(self): with mui.Paper(key=self._key, sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, elevation=1): @@ -85,4 +87,5 @@ def __call__(self): with mui.Stack(direction="row", spacing=2, alignItems="center", sx={"padding": "10px"}): mui.Button("Apply", variant="contained", onClick=sync()) + event.Hotkey("ctrl+s", sync(), bindInputs=True, overrideDefault=True) mui.Typography("Or press ctrl+s", sx={"flex": 1}) diff --git a/ui_components/file_explorer.py b/ui_components/file_explorer.py deleted file mode 100644 index f0037061..00000000 --- a/ui_components/file_explorer.py +++ /dev/null @@ -1,55 +0,0 @@ -import streamlit as st -from streamlit_elements import mui, elements - -from utils.os_utils import get_directories_from_directory, get_python_files_from_directory, \ - get_yml_files_from_directory, load_file, remove_file -from .dashboard import Dashboard - - -class FileExplorer(Dashboard.Item): - _directory = "hummingbot_files/bot_configs" - - @staticmethod - def set_selected_file(_, node_id): - st.session_state.selected_file = node_id - - @staticmethod - def delete_file(): - if st.session_state.selected_file: - if st.session_state.selected_file.endswith(".py") or st.session_state.selected_file.endswith(".yml"): - remove_file(st.session_state.selected_file) - else: - st.error("You can't delete the directory since it's a volume." - "If you want to do it, go to the orchestrate tab and delete the container") - - def edit_file(self): - short_path = st.session_state.selected_file.replace(self._directory, "") - language = "python" if st.session_state.selected_file.endswith(".py") else "yaml" - if st.session_state.selected_file.endswith(".py") or st.session_state.selected_file.endswith(".yml"): - st.session_state.editor_tabs[short_path] = {"content": load_file(st.session_state.selected_file), - "language": language, - "file_path": st.session_state.selected_file} - - def __call__(self): - bots = [bot.split("/")[-2] for bot in get_directories_from_directory(self._directory) if - "data_downloader" not in bot] - with mui.Paper(key=self._key, - sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, - elevation=1): - with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False): - with mui.Grid(container=True, spacing=4, sx={"display": "flex", "alignItems": "center"}): - with mui.Grid(item=True, xs=6, sx={"display": "flex", "alignItems": "center"}): - mui.icon.Folder() - mui.Typography("/bot_configs/") - with mui.Grid(item=True, xs=6, sx={"display": "flex", "justifyContent": "flex-end"}): - mui.IconButton(mui.icon.Delete, onClick=self.delete_file, sx={"mx": 1}) - mui.IconButton(mui.icon.Edit, onClick=self.edit_file, sx={"mx": 1}) - with mui.Box(sx={"overflow": "auto"}): - with mui.lab.TreeView(defaultExpandIcon=mui.icon.ChevronRight, defaultCollapseIcon=mui.icon.ExpandMore, - onNodeSelect=lambda event, node_id: self.set_selected_file(event, node_id)): - for bot in bots: - with mui.lab.TreeItem(nodeId=bot, label=f"πŸ€–{bot}"): - for file in get_python_files_from_directory(f"{self._directory}/{bot}/scripts"): - mui.lab.TreeItem(nodeId=file, label=f"🐍{file.split('/')[-1]}") - for file in get_yml_files_from_directory(f"{self._directory}/{bot}/conf/strategies"): - mui.lab.TreeItem(nodeId=file, label=f"πŸ“„ {file.split('/')[-1]}") diff --git a/ui_components/file_explorer_base.py b/ui_components/file_explorer_base.py new file mode 100644 index 00000000..066b5ced --- /dev/null +++ b/ui_components/file_explorer_base.py @@ -0,0 +1,63 @@ +import streamlit as st +from streamlit_elements import mui, elements + +from utils.os_utils import load_file, remove_file +from .dashboard import Dashboard + + +class FileExplorerBase(Dashboard.Item): + + def __init__(self, board, x, y, w, h, **item_props): + super().__init__(board, x, y, w, h, **item_props) + self._tabs = {} + self.selected_file = None + + def set_selected_file(self, _, node_id): + self.selected_file = node_id + + def delete_file(self): + if self.is_file_editable: + remove_file(self.selected_file) + else: + st.error("You can't delete the directory since it's a volume." + "If you want to do it, go to the orchestrate tab and delete the container") + + @property + def tabs(self): + return self._tabs + + def add_file_to_tab(self): + language = "python" if self.selected_file.endswith(".py") else "yaml" + if self.is_file_editable: + self._tabs[self.selected_file] = {"content": load_file(self.selected_file), + "language": language} + + def remove_file_from_tab(self): + if self.is_file_editable and self.selected_file in self._tabs: + del self._tabs[self.selected_file] + + @property + def is_file_editable(self): + return self.selected_file and \ + (self.selected_file.endswith(".py") or self.selected_file.endswith(".yml") + or "log" in self.selected_file) + + def add_tree_view(self): + raise NotImplementedError + + def __call__(self): + with mui.Paper(key=self._key, + sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, + elevation=1): + with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False): + with mui.Grid(container=True, spacing=4, sx={"display": "flex", "alignItems": "center"}): + with mui.Grid(item=True, xs=6, sx={"display": "flex", "alignItems": "center"}): + mui.icon.Folder() + mui.Typography("File Explorer") + with mui.Grid(item=True, xs=6, sx={"display": "flex", "justifyContent": "flex-end"}): + mui.IconButton(mui.icon.Delete, onClick=self.delete_file, sx={"mx": 1}) + mui.IconButton(mui.icon.Edit, onClick=self.add_file_to_tab, sx={"mx": 1}) + mui.IconButton(mui.icon.Close, onClick=self.remove_file_from_tab, sx={"mx": 1}) + with mui.Box(sx={"overflow": "auto"}): + self.add_tree_view() + diff --git a/ui_components/optimization_creation_card.py b/ui_components/optimization_creation_card.py new file mode 100644 index 00000000..d2247618 --- /dev/null +++ b/ui_components/optimization_creation_card.py @@ -0,0 +1,61 @@ +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_directional_strategies +from .dashboard import Dashboard + + +class OptimizationCreationCard(Dashboard.Item): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + today = datetime.datetime.now() + self._optimization_version = f"{today.day:02d}-{today.month:02d}-{today.year}" + self._optimization_name = None + self._strategy_name = None + + def _set_optimization_version(self, event): + self._optimization_version = event.target.value + + def _set_strategy_name(self, _, childs): + self._strategy_name = childs.props.value + + def _create_optimization(self, strategy_info): + strategy_code = strategy_optimization_template(strategy_info) + save_file(name=f"{self._strategy_name.lower()}_v_{self._optimization_version}.py", content=strategy_code, + path=constants.OPTIMIZATIONS_PATH) + + def __call__(self): + available_strategies = load_directional_strategies(constants.DIRECTIONAL_STRATEGIES_PATH) + strategy_names = list(available_strategies.keys()) + with mui.Paper(key=self._key, + sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, + elevation=1): + with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False): + mui.icon.NoteAdd() + mui.Typography("Create study", variant="h6") + if len(strategy_names) == 0: + mui.Alert("No strategies available, please create one to optimize it", severity="warning", + sx={"width": "100%"}) + return + else: + if self._strategy_name is None: + self._strategy_name = strategy_names[0] + with mui.Grid(container=True, spacing=2, sx={"padding": "10px"}): + with mui.Grid(item=True, xs=4): + with mui.FormControl(variant="standard", sx={"width": "100%"}): + mui.FormHelperText("Strategy name") + with mui.Select(label="Select strategy", defaultValue=strategy_names[0], + variant="standard", onChange=lazy(self._set_strategy_name)): + for strategy in strategy_names: + mui.MenuItem(strategy, value=strategy) + with mui.Grid(item=True, xs=4): + with mui.FormControl(variant="standard", sx={"width": "100%"}): + mui.TextField(defaultValue=self._optimization_version, label="Optimization version", + variant="standard", onChange=lazy(self._set_optimization_version)) + with mui.Grid(item=True, xs=4): + with mui.Button(variant="contained", onClick=lambda x: self._create_optimization( + available_strategies[self._strategy_name]), sx={"width": "100%"}): + mui.icon.Add() + mui.Typography("Create", variant="body1") diff --git a/ui_components/optimization_run_card.py b/ui_components/optimization_run_card.py new file mode 100644 index 00000000..91aa086c --- /dev/null +++ b/ui_components/optimization_run_card.py @@ -0,0 +1,66 @@ +import threading + +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 .dashboard import Dashboard + + +class OptimizationRunCard(Dashboard.Item): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._optimization_name = None + self._number_of_trials = 2000 + + def _set_optimization_name(self, _, childs): + self._optimization_name = childs.props.value + + def _set_number_of_trials(self, event): + self._number_of_trials = int(event.target.value) + + def _run_optimization(self): + study_name = self._optimization_name.split('/')[-1].split('.')[0] + study = optuna.create_study(direction="maximize", study_name=study_name, + storage="sqlite:///data/backtesting/backtesting_report.db", + load_if_exists=True) + objective = get_function_from_file(file_path=self._optimization_name, + function_name="objective") + + def optimization_process(): + study.optimize(objective, n_trials=self._number_of_trials) + + optimization_thread = threading.Thread(target=optimization_process) + optimization_thread.start() + + def __call__(self): + optimizations = get_python_files_from_directory(constants.OPTIMIZATIONS_PATH) + with mui.Paper(key=self._key, sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, elevation=1): + with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False): + mui.icon.AutoFixHigh() + mui.Typography("Run a optimization", variant="h6") + + if len(optimizations) == 0: + mui.Alert("No optimizations available, please create one.", severity="warning", sx={"width": "100%"}) + return + else: + if self._optimization_name is None: + self._optimization_name = optimizations[0] + with mui.Grid(container=True, spacing=2, sx={"padding": "10px"}): + with mui.Grid(item=True, xs=4): + with mui.FormControl(variant="standard", sx={"width": "100%"}): + mui.FormHelperText("Study name") + with mui.Select(defaultValue=optimizations[0], + variant="standard", onChange=lazy(self._set_optimization_name)): + for optimization in optimizations: + mui.MenuItem(f"{optimization.split('/')[-1].split('.')[0]}", value=optimization) + with mui.Grid(item=True, xs=4): + with mui.FormControl(variant="standard", sx={"width": "100%"}): + mui.TextField(defaultValue=self._optimization_name, label="Number of trials", type="number", + variant="standard", onChange=lazy(self._set_number_of_trials)) + with mui.Grid(item=True, xs=4): + with mui.Button(variant="contained", onClick=self._run_optimization, sx={"width": "100%"}): + mui.icon.PlayCircleFilled() + mui.Typography("Run", variant="button") diff --git a/ui_components/optimizations_file_explorer.py b/ui_components/optimizations_file_explorer.py new file mode 100644 index 00000000..c0354ab4 --- /dev/null +++ b/ui_components/optimizations_file_explorer.py @@ -0,0 +1,16 @@ +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 + + +class OptimizationsStrategiesFileExplorer(FileExplorerBase): + def add_tree_view(self): + with mui.lab.TreeView(defaultExpandIcon=mui.icon.ChevronRight, defaultCollapseIcon=mui.icon.ExpandMore, + onNodeSelect=lambda event, node_id: self.set_selected_file(event, node_id), + defaultExpanded=["optimization_strategies"]): + with mui.lab.TreeItem(nodeId="optimization_strategies", label=f"πŸ”¬Studies"): + optimizations = get_python_files_from_directory(constants.OPTIMIZATIONS_PATH) + for optimization in optimizations: + mui.lab.TreeItem(nodeId=optimization, label=f"🐍{optimization.split('/')[-1]}") diff --git a/utils/file_templates.py b/utils/file_templates.py index ff8be847..d4d55fcf 100644 --- a/utils/file_templates.py +++ b/utils/file_templates.py @@ -111,6 +111,7 @@ def objective(trial): trial.set_user_attr("profit_factor", strategy_analysis.profit_factor()) trial.set_user_attr("duration_in_hours", strategy_analysis.duration_in_minutes() / 60) trial.set_user_attr("avg_trading_time_in_hours", strategy_analysis.avg_trading_time_in_minutes() / 60) + trial.set_user_attr("config", config.dict()) return strategy_analysis.net_profit_pct() except Exception as e: traceback.print_exc() diff --git a/utils/os_utils.py b/utils/os_utils.py index 32dc9995..6dbf01ad 100644 --- a/utils/os_utils.py +++ b/utils/os_utils.py @@ -68,6 +68,11 @@ def get_python_files_from_directory(directory: str) -> list: return py_files +def get_log_files_from_directory(directory: str) -> list: + log_files = glob.glob(directory + "/**/*.log*", recursive=True) + return log_files + + def get_yml_files_from_directory(directory: str) -> list: yml = glob.glob(directory + "/**/*.yml", recursive=True) return yml diff --git a/utils/st_utils.py b/utils/st_utils.py index 52ff9518..5875109c 100644 --- a/utils/st_utils.py +++ b/utils/st_utils.py @@ -13,7 +13,6 @@ def initialize_st_page(title: str, icon: str, layout="wide", initial_sidebar_sta layout=layout, initial_sidebar_state=initial_sidebar_state ) - # st.title(f"{icon} {title}") caller_frame = inspect.currentframe().f_back add_page_title(layout="wide")