From 312289229664bcf670cbb0db4f028dbec542d94a Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 24 Aug 2024 12:58:55 +0100 Subject: [PATCH] Fix MODES to be Callables, add suspend/resume to screens, separate screen files --- ftui/__init__.py | 2 +- ftui/ftui.py | 69 +- ftui/ftui_client.py | 9 +- ftui/screens/dashboard_screen.py | 464 ++++++++++++ ftui/screens/help_screen.py | 54 ++ .../main_bot_screen.py} | 658 +----------------- ftui/screens/modal_screens.py | 98 +++ ftui/screens/settings_screen.py | 78 +++ ftui/widgets/timed_screen.py | 19 + pyproject.toml | 2 +- 10 files changed, 744 insertions(+), 709 deletions(-) create mode 100644 ftui/screens/dashboard_screen.py create mode 100644 ftui/screens/help_screen.py rename ftui/{ftui_screens.py => screens/main_bot_screen.py} (57%) create mode 100644 ftui/screens/modal_screens.py create mode 100644 ftui/screens/settings_screen.py create mode 100644 ftui/widgets/timed_screen.py diff --git a/ftui/__init__.py b/ftui/__init__.py index ae73625..bbab024 100644 --- a/ftui/__init__.py +++ b/ftui/__init__.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" diff --git a/ftui/ftui.py b/ftui/ftui.py index 51fcc28..390a090 100644 --- a/ftui/ftui.py +++ b/ftui/ftui.py @@ -30,13 +30,11 @@ import ftui.ftui_client as ftuic import ftui.ftui_helpers as fth -from ftui.ftui_screens import ( - DashboardScreen, - HelpScreen, - MainBotScreen, - SettingsScreen, - TradeInfoScreen, -) +from ftui.screens.dashboard_screen import DashboardScreen +from ftui.screens.help_screen import HelpScreen +from ftui.screens.main_bot_screen import MainBotScreen +from ftui.screens.modal_screens import TradeInfoScreen +from ftui.screens.settings_screen import SettingsScreen urlre = r"^\[([a-zA-Z0-9]+)\]*([a-zA-Z0-9\-._~%!$&'()*+,;=]+)?:([ a-zA-Z0-9\-._~%!$&'()*+,;=]+)@?([a-z0-9\-._~%]+|\[[a-f0-9:.]+\]|\[v[a-f0-9][a-z0-9\-._~%!$&'()*+,;=:]+\]):([0-9]+)?" @@ -92,21 +90,21 @@ class FreqText(App): loglimit = 100 - # setup screens - dash_screen = DashboardScreen() + # # setup screens + # dash_screen = DashboardScreen() - bot_screen = MainBotScreen() + # bot_screen = MainBotScreen() - settings_screen = SettingsScreen() - # settings_screen.set_settings(settings) + # settings_screen = SettingsScreen() + # # settings_screen.set_settings(settings) - help_screen = HelpScreen() + # help_screen = HelpScreen() MODES = { - "dashboard": dash_screen, - "bots": bot_screen, - "settings": settings_screen, - "help": help_screen, + "dashboard": DashboardScreen, + "bots": MainBotScreen, + "settings": SettingsScreen, + "help": HelpScreen, } # supported colours: https://textual.textualize.io/api/color/ @@ -394,37 +392,9 @@ def watch_show_clients(self, show_clients: bool) -> None: self.set_class(show_clients, "-show-clients") # ACTIONS - async def action_switch_to_bot(self, bot_id) -> None: - current_screen = self.screen - - for ts in current_screen.timers.keys(): - print(f"Pausing {current_screen.id} {ts}") - current_screen.timers[ts].pause() - - await self.switch_mode("bots") - - for ts in self.MODES["bots"].timers.keys(): - print(f"Resuming bots {ts}") - self.MODES["bots"].timers[ts].resume() - - self.MODES["bots"].update_select_options(bot_id) - async def action_switch_ftui_mode(self, mode) -> None: - current_screen = self.screen - - for ts in current_screen.timers.keys(): - print(f"Pausing {current_screen.id} {ts}") - current_screen.timers[ts].pause() - - for ts in self.MODES[mode].timers.keys(): - print(f"Resuming {mode} {ts}") - self.MODES[mode].timers[ts].resume() - await self.switch_mode(mode) - if mode == "bots": - self.MODES["bots"].update_select_options() - def action_update_chart(self, bot_id, pair) -> None: self.MODES["bots"].update_chart(bot_id, pair) @@ -437,13 +407,6 @@ def action_show_trade_info_dialog(self, trade_id, cl_name) -> None: tis.client = self.client_dict[cl_name] self.push_screen(tis) - # def action_open_link(self, link) -> None: - # try: - # webbrowser.open(link, new=2) - # except Exception as e: - # print(f"Error opening link: {e}") - # pass - def setup(args): config = args.config @@ -455,7 +418,7 @@ def setup(args): for s in args.servers: try: ftui_client = ftuic.FTUIClient( - name=s["name"], + name=s["name"] if "name" in s else None, url=s["ip"], port=s["port"], username=s["username"], diff --git a/ftui/ftui_client.py b/ftui/ftui_client.py index 6054fab..60d559b 100644 --- a/ftui/ftui_client.py +++ b/ftui/ftui_client.py @@ -60,8 +60,8 @@ def setup_client(self): self.username = config.get("api_server", {}).get("username") self.password = config.get("api_server", {}).get("password") - if self.name is None: - self.name = f"{self.url}:{self.port}" + #if self.name is None: + # self.name = f"{self.url}:{self.port}" server_url = f"http://{self.url}:{self.port}" @@ -89,7 +89,10 @@ def setup_client(self): self.rest_client = client current_config = self.get_client_config() - self.name = current_config.get("bot_name", self.name) + self.name = current_config.get( + "bot_name", + f"{self.url}:{self.port}" + ) if self.name is None else self.name bot_state = current_config["state"] runmode = current_config["runmode"] strategy = current_config["strategy"] diff --git a/ftui/screens/dashboard_screen.py b/ftui/screens/dashboard_screen.py new file mode 100644 index 0000000..6b70670 --- /dev/null +++ b/ftui/screens/dashboard_screen.py @@ -0,0 +1,464 @@ +from datetime import datetime + +import pandas as pd +from rich.table import Table +from rich.text import Text +from textual import on, work +from textual.app import ComposeResult +from textual.containers import Container +from textual.widgets import ( + Collapsible, + Digits, + Footer, + Header, + Label, + Static, +) +from textual.worker import get_current_worker +from textual_plotext import PlotextPlot + +import ftui.ftui_helpers as fth +from ftui.widgets.timed_screen import TimedScreen + + +class DashboardScreen(TimedScreen): + + + COLLAP_FUNC_MAP = { + # collapsibles + "dsh-cp-collap": "update_cumulative_profit_plot", + } + + def compose(self) -> ComposeResult: + yield Header(show_clock=True) + + with Container(id="above"): + with Container(id="all-open-profit"): + yield Label("Open") + yield Digits(id="all-bot-summary-open-profit") + + with Container(id="all-closed-profit"): + yield Label("Closed") + yield Digits(id="all-bot-summary-closed-profit") + + with Container(id="all-daily-profit"): + yield Label("Daily") + yield Digits(id="all-bot-summary-daily-profit") + yield Static(id="yesterday-profit") + + with Container(id="all-weekly-profit"): + yield Label("Weekly") + yield Digits(id="all-bot-summary-weekly-profit") + yield Static(id="last-week-profit") + + with Container(id="all-monthly-profit"): + yield Label("Monthly") + yield Digits(id="all-bot-summary-monthly-profit") + yield Static(id="last-month-profit") + + with Container(id="parent-container"): + with Container(id="dash-container"): + with Container(id="dash-all-trade-summary"): + yield Static(id="all-trade-summary-table") + + with Container(id="dash-collapsibles"): + with Collapsible(title="All Open Trades", id="dsh-op-collap", collapsed=False): + yield Static(id="all-open-trades-table", classes="bg-static-default") + + with Collapsible(title="All Closed Trades", id="dsh-cl-collap", collapsed=True): + with Container(id="dash-closed-profit-container"): + yield Static(id="dash-closed-profit", classes="bg-static-default") + + with Collapsible(title="Cumulative Profit", id="dsh-cp-collap", collapsed=True): + yield PlotextPlot(id="dash-cumprof-profit", classes="bg-static-default") + + # with Collapsible(title="Daily Trade Summary", + # id="dsh-dt-collap", + # collapsed=True): + # yield Static( + # id="dash-daily-profit" + # ) + + yield Footer() + + def on_mount(self) -> None: + ats = self.query_one("#dash-all-trade-summary") + ats.loading = True + + summary_digits = self.query_one("#above").query(Digits) + for sd in summary_digits: + sd.loading = True + + update_one_sec_render = self.set_interval(1, self.update_per_sec) + self.timers["1sec"] = update_one_sec_render + + update_five_sec_render = self.set_interval(5, self.update_per_five_sec) + self.timers["5sec"] = update_five_sec_render + + async def update_per_sec(self): + self.update_dashboard_all_bot_summary() + + dsh_op_collap = self.query_one("#dsh-op-collap") + if dsh_op_collap.collapsed is False: + self.update_dashboard_all_open_trades() + + async def update_per_five_sec(self): + self.update_dashboard_all_trade_summary() + + dsh_cl_collap = self.query_one("#dsh-cl-collap") + if dsh_cl_collap.collapsed is False: + self.update_dashboard_all_closed_trades() + + dsh_cp_collap = self.query_one("#dsh-cp-collap") + if dsh_cp_collap.collapsed is False: + self.update_cumulative_profit_plot() + + def _render_open_trade_data(self, data, trading_mode="spot"): + row_data = [] + + for idx, v in data.iterrows(): + # bot_name = v['Bot'] + + render_data = ( + # f"[@click=app.switch_to_bot('{bot_name}')]{bot_name}[/]", + f"{v['Bot']}", + f"{v['ID']}", + f"{v['Pair']}", + f"{round(v['Stake Amount'], 3)}", + ) + + if trading_mode != "spot": + render_data = render_data + (f"{v['Leverage']}") + + render_data = render_data + ( + f"{v['# Orders']}", + f"{round(v['Open Rate'], 3)}", + f"{v['Current Rate']}", + fth.red_or_green(float(v["Stop %"])), + fth.red_or_green(float(v["Max %"]), justify="left"), + fth.red_or_green(float(v["Profit %"]), justify="right"), + fth.red_or_green(float(v["Profit"]), justify="left"), + str(v["Dur."]).split(".")[0].replace("0 days ", ""), + f"{v['S/L']}", + f"{v['Entry']}", + ) + + row_data.append(render_data) + + return row_data + + def _render_closed_trade_data(self, data): + row_data = [] + data = data.sort_values(by="Close Date", ascending=False) + + for idx, v in data.iterrows(): + row_data.append( + ( + f"{v['Bot']}", + f"{v['ID']}", + f"{v['Pair']}", + fth.red_or_green(float(v["Profit %"]), justify="right"), + fth.red_or_green(float(v["Profit"]), justify="right"), + f"[cyan]{str(v['Open Date']).split('+')[0]}", + str(v["Dur."]).split(".")[0].replace("0 days ", ""), + f"{v['Entry']}", + f"{v['Exit']}", + ) + ) + + return row_data + + @work(group="dash_all_summary_worker", exclusive=True, thread=True) + def update_dashboard_all_bot_summary(self): + closed_profit = 0 + open_profit = 0 + daily_profit = 0 + yesterday_profit = 0 + weekly_profit = 0 + last_week_profit = 0 + monthly_profit = 0 + last_month_profit = 0 + + client_dict = self.app.client_dict + client_dfs = self.app.client_dfs + + for n, cl in client_dict.items(): + open_data = fth.get_open_dataframe_data(cl, client_dfs) + closed_data = fth.get_closed_dataframe_data(cl, client_dfs) + + tot_profit = 0 + if not open_data.empty: + tot_profit = round(open_data["Profit"].sum(), 2) + + pcc = 0 + if not closed_data.empty: + pcc = round(closed_data["Profit"].sum(), 2) + + closed_profit = closed_profit + pcc + open_profit = open_profit + tot_profit + + d = cl.get_daily_profit(days=2) + if d is not None and "data" in d and d["data"]: + daily_profit = daily_profit + d["data"][0]["abs_profit"] + yesterday_profit = d["data"][1]["abs_profit"] + w = cl.get_weekly_profit(weeks=2) + if w is not None and "data" in w and w["data"]: + weekly_profit = weekly_profit + w["data"][0]["abs_profit"] + last_week_profit = w["data"][1]["abs_profit"] + m = cl.get_monthly_profit(months=2) + if m is not None and "data" in m and m["data"]: + monthly_profit = monthly_profit + m["data"][0]["abs_profit"] + last_month_profit = m["data"][1]["abs_profit"] + + cps = round(closed_profit, 2) + ops = round(open_profit, 2) + dps = round(daily_profit, 2) + wps = round(weekly_profit, 2) + mps = round(monthly_profit, 2) + + opsd = self.query_one("#all-bot-summary-open-profit") + cpsd = self.query_one("#all-bot-summary-closed-profit") + dpsd = self.query_one("#all-bot-summary-daily-profit") + wpsd = self.query_one("#all-bot-summary-weekly-profit") + mpsd = self.query_one("#all-bot-summary-monthly-profit") + ypsd = self.query_one("#yesterday-profit") + lwpsd = self.query_one("#last-week-profit") + lmpsd = self.query_one("#last-month-profit") + + worker = get_current_worker() + if not worker.is_cancelled: + self.app.call_from_thread(opsd.update, f"{ops}") + self.app.call_from_thread(cpsd.update, f"{cps}") + self.app.call_from_thread(dpsd.update, f"{dps}") + self.app.call_from_thread(wpsd.update, f"{wps}") + self.app.call_from_thread(mpsd.update, f"{mps}") + self.app.call_from_thread( + ypsd.update, Text(f"{round(yesterday_profit, 2)}", justify="center") + ) + self.app.call_from_thread( + lwpsd.update, Text(f"{round(last_week_profit, 2)}", justify="center") + ) + self.app.call_from_thread( + lmpsd.update, Text(f"{round(last_month_profit, 2)}", justify="center") + ) + + fth.set_red_green_widget_colour(opsd, ops) + fth.set_red_green_widget_colour(cpsd, cps) + fth.set_red_green_widget_colour(dpsd, dps) + fth.set_red_green_widget_colour(ypsd, yesterday_profit) + fth.set_red_green_widget_colour(wpsd, wps) + fth.set_red_green_widget_colour(lwpsd, last_week_profit) + fth.set_red_green_widget_colour(mpsd, mps) + fth.set_red_green_widget_colour(lmpsd, last_month_profit) + + opsd.loading = False + cpsd.loading = False + dpsd.loading = False + wpsd.loading = False + mpsd.loading = False + + @work(group="dash_all_open_worker", exclusive=True, thread=True) + def update_dashboard_all_open_trades(self): + client_dict = self.app.client_dict + client_dfs = self.app.client_dfs + + all_open_df = pd.DataFrame() + for n, cl in client_dict.items(): + if cl.name in client_dfs and "op_data" in client_dfs[cl.name]: + data = client_dfs[cl.name]["op_data"].copy() + if not data.empty: + all_open_df = pd.concat([all_open_df, data]) + + row_data = self._render_open_trade_data(all_open_df) + + dt = self.query_one("#all-open-trades-table") + table = fth.dash_open_trades_table( + row_data, trading_mode=cl.get_client_config().get("trading_mode") + ) + + worker = get_current_worker() + if not worker.is_cancelled: + self.app.call_from_thread(dt.update, table) + dt.loading = False + + @work(group="dash_all_closed_worker", exclusive=True, thread=True) + def update_dashboard_all_closed_trades(self, num_closed_trades=5) -> Table: + client_dict = self.app.client_dict + client_dfs = self.app.client_dfs + + all_closed_df = pd.DataFrame() + for n, cl in client_dict.items(): + if cl.name in client_dfs and "cl_data" in client_dfs[cl.name]: + data = client_dfs[cl.name]["cl_data"].copy() + all_closed_df = pd.concat([all_closed_df, data[:num_closed_trades]]) + + row_data = self._render_closed_trade_data(all_closed_df) + + dt = self.query_one("#dash-closed-profit") + table = fth.dash_closed_trades_table(row_data) + + worker = get_current_worker() + if not worker.is_cancelled: + self.app.call_from_thread(dt.update, table) + dt.loading = False + + @work(group="dash_all_trade_worker", exclusive=False, thread=True) + def update_dashboard_all_trade_summary(self): + client_dict = self.app.client_dict + client_dfs = self.app.client_dfs + + row_data = [ + # ("Bot", "# Trades", "Open Profit", "W/L", "Winrate", "Exp.", "Exp. Rate", "Med W", "Med L", "Tot. Profit"), + ] + all_open_profit = 0 + all_profit = 0 + all_wins = 0 + all_losses = 0 + + for n, cl in client_dict.items(): + open_data = fth.get_open_dataframe_data(cl, client_dfs) + closed_data = fth.get_closed_dataframe_data(cl, client_dfs) + + open_profit = 0 + mean_prof_w = 0 + mean_prof_l = 0 + median_win = 0 + median_loss = 0 + + tpw = [] + tpl = [] + + if not open_data.empty: + open_profit = round(open_data["Profit"].sum(), 2) + + if "Profit" in closed_data.columns: + tpw = closed_data.loc[closed_data["Profit"] >= 0, "Profit"] + tpl = closed_data.loc[closed_data["Profit"] < 0, "Profit"] + + if len(tpw) > 0: + mean_prof_w = round(tpw.mean(), 2) + median_win = round(tpw.median(), 2) + + if len(tpl) > 0: + mean_prof_l = round(tpl.mean(), 2) + median_loss = round(tpl.median(), 2) + + if (len(tpw) == 0) and (len(tpl) == 0): + winrate = 0 + loserate = 0 + else: + winrate = (len(tpw) / (len(tpw) + len(tpl))) * 100 + loserate = 100 - winrate + + expectancy_ratio = float("inf") + if abs(mean_prof_l) > 0: + expectancy_ratio = ((1 + (mean_prof_w / abs(mean_prof_l))) * (winrate / 100)) - 1 + + expectancy = ((winrate / 100) * mean_prof_w) - ((loserate / 100) * mean_prof_l) + + t = cl.get_total_profit() + if t is None: + return [] + + pcc = round(float(t["profit_closed_coin"]), 2) + bot_start_date = datetime.strptime( + f"{t['bot_start_date']}+00:00", self.app.TZFMT + ).date() + + all_open_profit = all_open_profit + open_profit + all_profit = all_profit + pcc + all_wins = all_wins + t["winning_trades"] + all_losses = all_losses + t["losing_trades"] + + trade_cnt_str = ( + f"[cyan]{int(t['trade_count'])-int(t['closed_trade_count'])}" + f"[white]/[purple]{t['closed_trade_count']}" + ) + + row_data.append( + ( + f"{n}", + f"{bot_start_date}", + trade_cnt_str, + fth.red_or_green(round(open_profit, 2), justify="right"), + f"[green]{t['winning_trades']}/[red]{t['losing_trades']}", + f"[cyan]{round(winrate, 1)}", + f"[purple]{round(expectancy, 2)}", + fth.red_or_green(round(expectancy_ratio, 2)), + fth.red_or_green( + round(median_win, 2), justify="right" + ), # f"[green]{median_win}", + fth.red_or_green( + round(median_loss, 2), justify="left" + ), # f"[red]{median_loss}", + fth.red_or_green(pcc, justify="right"), + ) + ) + + footer = { + "all_open_profit": fth.red_or_green(round(all_open_profit, 2), justify="right"), + "num_wins_losses": f"[green]{all_wins}/[red]{all_losses}", + "all_total_profit": fth.red_or_green(round(all_profit, 2), justify="right"), + } + + dt = self.query_one("#all-trade-summary-table") + table = fth.dash_trades_summary(row_data, footer) + + worker = get_current_worker() + if not worker.is_cancelled: + self.app.call_from_thread(dt.update, table) + + ats = self.query_one("#dash-all-trade-summary") + ats.loading = False + + @work(group="dash_chart_worker", exclusive=True, thread=True) + def update_cumulative_profit_plot(self): + client_dfs = self.app.client_dfs + + all_cum_data = pd.DataFrame() + if "all_closed" in client_dfs: + all_cum_data = fth.dash_cumulative_profit_plot_data(client_dfs["all_closed"]) + + if "plot_cumprof" in all_cum_data.columns: + chart_container = self.query_one("#dash-cumprof-profit") + cplt = chart_container.plt + dfmt = "Y-m-d" + cplt.date_form(dfmt) + + all_cum_data.index = all_cum_data.index.tz_localize(None) + + dates = cplt.datetimes_to_string(all_cum_data.index) + + cplt.plot( + dates, + all_cum_data["plot_cumprof"].values, + color=self.app.COLOURS.profit_chart_col, + ) + + cplt.ylim( + all_cum_data["plot_cumprof"].min() * 0.99, + all_cum_data["plot_cumprof"].max() * 1.01, + ) + cplt.ylabel("Profit") + + worker = get_current_worker() + if not worker.is_cancelled: + self.app.call_from_thread(chart_container.refresh) + chart_container.loading = False + + @on(Collapsible.Toggled) + def toggle_collapsible(self, event: Collapsible.Toggled) -> None: + event.stop() + collap = event.collapsible + + collap_children = collap.query().filter(".bg-static-default") + + if collap.collapsed is False: + for child in collap_children: + child.loading = True + + if collap.id in self.COLLAP_FUNC_MAP: + getattr(self, self.COLLAP_FUNC_MAP[collap.id])() + else: + for child in collap_children: + child.loading = False diff --git a/ftui/screens/help_screen.py b/ftui/screens/help_screen.py new file mode 100644 index 0000000..59e4ff0 --- /dev/null +++ b/ftui/screens/help_screen.py @@ -0,0 +1,54 @@ +from pathlib import Path + +from textual.app import ComposeResult +from textual.containers import Container +from textual.screen import Screen +from textual.widgets import ( + Footer, + Header, + Static, +) + +from ftui.widgets.linkable_markdown_viewer import LinkableMarkdown + + +class HelpScreen(Screen): + help_file_path = Path(__file__).parent / "md" / "help.md" + + header_str = """ + ███████╗████████╗██╗ ██╗██╗ + ██╔════╝╚══██╔══╝██║ ██║██║ + █████╗ ██║ ██║ ██║██║ + ██╔══╝ ██║ ██║ ██║██║ + ██║ ██║ ╚██████╔╝██║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ + """ + + @property + def markdown_viewer(self) -> LinkableMarkdown: + """Get the Markdown widget.""" + return self.query_one(LinkableMarkdown) + + def compose(self) -> ComposeResult: + yield Header(show_clock=True) + + with Container(id="help-above"): + yield Static(self.header_str) + + with Container(id="parent-container"): + with Container(id="right"): + yield LinkableMarkdown() + + yield Footer() + + async def on_mount(self) -> None: + self.markdown_viewer.focus() + try: + await self.markdown_viewer.load(self.help_file_path) + except FileNotFoundError: + msg = f"Unable to load help file: {self.help_file_path!r}" + self.notify( + msg, + title="Error:", + severity="warning", + ) diff --git a/ftui/ftui_screens.py b/ftui/screens/main_bot_screen.py similarity index 57% rename from ftui/ftui_screens.py rename to ftui/screens/main_bot_screen.py index 55cd64f..89823bb 100644 --- a/ftui/ftui_screens.py +++ b/ftui/screens/main_bot_screen.py @@ -1,23 +1,16 @@ from datetime import datetime, timezone -from pathlib import Path import numpy as np import pandas as pd -from rich.table import Table -from rich.text import Text from textual import on, work from textual.app import ComposeResult from textual.containers import Container, Horizontal, Vertical -from textual.screen import ModalScreen, Screen +from textual.events import ScreenResume from textual.widgets import ( Button, - Checkbox, Collapsible, - DataTable, - Digits, Footer, Header, - Input, Label, ListView, Log, @@ -31,456 +24,13 @@ from textual.worker import get_current_worker from textual_plotext import PlotextPlot -import ftui.ftui_client as ftuic import ftui.ftui_helpers as fth from ftui.widgets.label_item import LabelItem from ftui.widgets.linkable_markdown_viewer import LinkableMarkdown +from ftui.widgets.timed_screen import TimedScreen -class DashboardScreen(Screen): - timers = {} - - COLLAP_FUNC_MAP = { - # collapsibles - "dsh-cp-collap": "update_cumulative_profit_plot", - } - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - - with Container(id="above"): - with Container(id="all-open-profit"): - yield Label("Open") - yield Digits(id="all-bot-summary-open-profit") - - with Container(id="all-closed-profit"): - yield Label("Closed") - yield Digits(id="all-bot-summary-closed-profit") - - with Container(id="all-daily-profit"): - yield Label("Daily") - yield Digits(id="all-bot-summary-daily-profit") - yield Static(id="yesterday-profit") - - with Container(id="all-weekly-profit"): - yield Label("Weekly") - yield Digits(id="all-bot-summary-weekly-profit") - yield Static(id="last-week-profit") - - with Container(id="all-monthly-profit"): - yield Label("Monthly") - yield Digits(id="all-bot-summary-monthly-profit") - yield Static(id="last-month-profit") - - with Container(id="parent-container"): - with Container(id="dash-container"): - with Container(id="dash-all-trade-summary"): - yield Static(id="all-trade-summary-table") - - with Container(id="dash-collapsibles"): - with Collapsible(title="All Open Trades", id="dsh-op-collap", collapsed=False): - yield Static(id="all-open-trades-table", classes="bg-static-default") - - with Collapsible(title="All Closed Trades", id="dsh-cl-collap", collapsed=True): - with Container(id="dash-closed-profit-container"): - yield Static(id="dash-closed-profit", classes="bg-static-default") - - with Collapsible(title="Cumulative Profit", id="dsh-cp-collap", collapsed=True): - yield PlotextPlot(id="dash-cumprof-profit", classes="bg-static-default") - - # with Collapsible(title="Daily Trade Summary", - # id="dsh-dt-collap", - # collapsed=True): - # yield Static( - # id="dash-daily-profit" - # ) - - yield Footer() - - def on_mount(self) -> None: - ats = self.query_one("#dash-all-trade-summary") - ats.loading = True - - summary_digits = self.query_one("#above").query(Digits) - for sd in summary_digits: - sd.loading = True - - update_one_sec_render = self.set_interval(1, self.update_per_sec) - self.timers["1sec"] = update_one_sec_render - - update_five_sec_render = self.set_interval(5, self.update_per_five_sec) - self.timers["5sec"] = update_five_sec_render - - async def update_per_sec(self): - self.update_dashboard_all_bot_summary() - - dsh_op_collap = self.query_one("#dsh-op-collap") - if dsh_op_collap.collapsed is False: - self.update_dashboard_all_open_trades() - - async def update_per_five_sec(self): - self.update_dashboard_all_trade_summary() - - dsh_cl_collap = self.query_one("#dsh-cl-collap") - if dsh_cl_collap.collapsed is False: - self.update_dashboard_all_closed_trades() - - dsh_cp_collap = self.query_one("#dsh-cp-collap") - if dsh_cp_collap.collapsed is False: - self.update_cumulative_profit_plot() - - def _render_open_trade_data(self, data, trading_mode="spot"): - row_data = [] - - for idx, v in data.iterrows(): - # bot_name = v['Bot'] - - render_data = ( - # f"[@click=app.switch_to_bot('{bot_name}')]{bot_name}[/]", - f"{v['Bot']}", - f"{v['ID']}", - f"{v['Pair']}", - f"{round(v['Stake Amount'], 3)}", - ) - - if trading_mode != "spot": - render_data = render_data + (f"{v['Leverage']}") - - render_data = render_data + ( - f"{v['# Orders']}", - f"{round(v['Open Rate'], 3)}", - f"{v['Current Rate']}", - fth.red_or_green(float(v["Stop %"])), - fth.red_or_green(float(v["Max %"]), justify="left"), - fth.red_or_green(float(v["Profit %"]), justify="right"), - fth.red_or_green(float(v["Profit"]), justify="left"), - str(v["Dur."]).split(".")[0].replace("0 days ", ""), - f"{v['S/L']}", - f"{v['Entry']}", - ) - - row_data.append(render_data) - - return row_data - - def _render_closed_trade_data(self, data): - row_data = [] - data = data.sort_values(by="Close Date", ascending=False) - - for idx, v in data.iterrows(): - row_data.append( - ( - f"{v['Bot']}", - f"{v['ID']}", - f"{v['Pair']}", - fth.red_or_green(float(v["Profit %"]), justify="right"), - fth.red_or_green(float(v["Profit"]), justify="right"), - f"[cyan]{str(v['Open Date']).split('+')[0]}", - str(v["Dur."]).split(".")[0].replace("0 days ", ""), - f"{v['Entry']}", - f"{v['Exit']}", - ) - ) - - return row_data - - @work(group="dash_all_summary_worker", exclusive=True, thread=True) - def update_dashboard_all_bot_summary(self): - closed_profit = 0 - open_profit = 0 - daily_profit = 0 - yesterday_profit = 0 - weekly_profit = 0 - last_week_profit = 0 - monthly_profit = 0 - last_month_profit = 0 - - client_dict = self.app.client_dict - client_dfs = self.app.client_dfs - - for n, cl in client_dict.items(): - open_data = fth.get_open_dataframe_data(cl, client_dfs) - closed_data = fth.get_closed_dataframe_data(cl, client_dfs) - - tot_profit = 0 - if not open_data.empty: - tot_profit = round(open_data["Profit"].sum(), 2) - - pcc = 0 - if not closed_data.empty: - pcc = round(closed_data["Profit"].sum(), 2) - - closed_profit = closed_profit + pcc - open_profit = open_profit + tot_profit - - d = cl.get_daily_profit(days=2) - if d is not None and "data" in d and d["data"]: - daily_profit = daily_profit + d["data"][0]["abs_profit"] - yesterday_profit = d["data"][1]["abs_profit"] - w = cl.get_weekly_profit(weeks=2) - if w is not None and "data" in w and w["data"]: - weekly_profit = weekly_profit + w["data"][0]["abs_profit"] - last_week_profit = w["data"][1]["abs_profit"] - m = cl.get_monthly_profit(months=2) - if m is not None and "data" in m and m["data"]: - monthly_profit = monthly_profit + m["data"][0]["abs_profit"] - last_month_profit = m["data"][1]["abs_profit"] - - cps = round(closed_profit, 2) - ops = round(open_profit, 2) - dps = round(daily_profit, 2) - wps = round(weekly_profit, 2) - mps = round(monthly_profit, 2) - - opsd = self.query_one("#all-bot-summary-open-profit") - cpsd = self.query_one("#all-bot-summary-closed-profit") - dpsd = self.query_one("#all-bot-summary-daily-profit") - wpsd = self.query_one("#all-bot-summary-weekly-profit") - mpsd = self.query_one("#all-bot-summary-monthly-profit") - ypsd = self.query_one("#yesterday-profit") - lwpsd = self.query_one("#last-week-profit") - lmpsd = self.query_one("#last-month-profit") - - worker = get_current_worker() - if not worker.is_cancelled: - self.app.call_from_thread(opsd.update, f"{ops}") - self.app.call_from_thread(cpsd.update, f"{cps}") - self.app.call_from_thread(dpsd.update, f"{dps}") - self.app.call_from_thread(wpsd.update, f"{wps}") - self.app.call_from_thread(mpsd.update, f"{mps}") - self.app.call_from_thread( - ypsd.update, Text(f"{round(yesterday_profit, 2)}", justify="center") - ) - self.app.call_from_thread( - lwpsd.update, Text(f"{round(last_week_profit, 2)}", justify="center") - ) - self.app.call_from_thread( - lmpsd.update, Text(f"{round(last_month_profit, 2)}", justify="center") - ) - - fth.set_red_green_widget_colour(opsd, ops) - fth.set_red_green_widget_colour(cpsd, cps) - fth.set_red_green_widget_colour(dpsd, dps) - fth.set_red_green_widget_colour(ypsd, yesterday_profit) - fth.set_red_green_widget_colour(wpsd, wps) - fth.set_red_green_widget_colour(lwpsd, last_week_profit) - fth.set_red_green_widget_colour(mpsd, mps) - fth.set_red_green_widget_colour(lmpsd, last_month_profit) - - opsd.loading = False - cpsd.loading = False - dpsd.loading = False - wpsd.loading = False - mpsd.loading = False - - @work(group="dash_all_open_worker", exclusive=True, thread=True) - def update_dashboard_all_open_trades(self): - client_dict = self.app.client_dict - client_dfs = self.app.client_dfs - - all_open_df = pd.DataFrame() - for n, cl in client_dict.items(): - if cl.name in client_dfs and "op_data" in client_dfs[cl.name]: - data = client_dfs[cl.name]["op_data"].copy() - if not data.empty: - all_open_df = pd.concat([all_open_df, data]) - - row_data = self._render_open_trade_data(all_open_df) - - dt = self.query_one("#all-open-trades-table") - table = fth.dash_open_trades_table( - row_data, trading_mode=cl.get_client_config().get("trading_mode") - ) - - worker = get_current_worker() - if not worker.is_cancelled: - self.app.call_from_thread(dt.update, table) - dt.loading = False - - @work(group="dash_all_closed_worker", exclusive=True, thread=True) - def update_dashboard_all_closed_trades(self, num_closed_trades=5) -> Table: - client_dict = self.app.client_dict - client_dfs = self.app.client_dfs - - all_closed_df = pd.DataFrame() - for n, cl in client_dict.items(): - if cl.name in client_dfs and "cl_data" in client_dfs[cl.name]: - data = client_dfs[cl.name]["cl_data"].copy() - all_closed_df = pd.concat([all_closed_df, data[:num_closed_trades]]) - - row_data = self._render_closed_trade_data(all_closed_df) - - dt = self.query_one("#dash-closed-profit") - table = fth.dash_closed_trades_table(row_data) - - worker = get_current_worker() - if not worker.is_cancelled: - self.app.call_from_thread(dt.update, table) - dt.loading = False - - @work(group="dash_all_trade_worker", exclusive=False, thread=True) - def update_dashboard_all_trade_summary(self): - client_dict = self.app.client_dict - client_dfs = self.app.client_dfs - - row_data = [ - # ("Bot", "# Trades", "Open Profit", "W/L", "Winrate", "Exp.", "Exp. Rate", "Med W", "Med L", "Tot. Profit"), - ] - all_open_profit = 0 - all_profit = 0 - all_wins = 0 - all_losses = 0 - - for n, cl in client_dict.items(): - open_data = fth.get_open_dataframe_data(cl, client_dfs) - closed_data = fth.get_closed_dataframe_data(cl, client_dfs) - - open_profit = 0 - mean_prof_w = 0 - mean_prof_l = 0 - median_win = 0 - median_loss = 0 - - tpw = [] - tpl = [] - - if not open_data.empty: - open_profit = round(open_data["Profit"].sum(), 2) - - if "Profit" in closed_data.columns: - tpw = closed_data.loc[closed_data["Profit"] >= 0, "Profit"] - tpl = closed_data.loc[closed_data["Profit"] < 0, "Profit"] - - if len(tpw) > 0: - mean_prof_w = round(tpw.mean(), 2) - median_win = round(tpw.median(), 2) - - if len(tpl) > 0: - mean_prof_l = round(tpl.mean(), 2) - median_loss = round(tpl.median(), 2) - - if (len(tpw) == 0) and (len(tpl) == 0): - winrate = 0 - loserate = 0 - else: - winrate = (len(tpw) / (len(tpw) + len(tpl))) * 100 - loserate = 100 - winrate - - expectancy_ratio = float("inf") - if abs(mean_prof_l) > 0: - expectancy_ratio = ((1 + (mean_prof_w / abs(mean_prof_l))) * (winrate / 100)) - 1 - - expectancy = ((winrate / 100) * mean_prof_w) - ((loserate / 100) * mean_prof_l) - - t = cl.get_total_profit() - if t is None: - return [] - - pcc = round(float(t["profit_closed_coin"]), 2) - bot_start_date = datetime.strptime( - f"{t['bot_start_date']}+00:00", self.app.TZFMT - ).date() - - all_open_profit = all_open_profit + open_profit - all_profit = all_profit + pcc - all_wins = all_wins + t["winning_trades"] - all_losses = all_losses + t["losing_trades"] - - trade_cnt_str = ( - f"[cyan]{int(t['trade_count'])-int(t['closed_trade_count'])}" - f"[white]/[purple]{t['closed_trade_count']}" - ) - - row_data.append( - ( - f"{n}", - f"{bot_start_date}", - trade_cnt_str, - fth.red_or_green(round(open_profit, 2), justify="right"), - f"[green]{t['winning_trades']}/[red]{t['losing_trades']}", - f"[cyan]{round(winrate, 1)}", - f"[purple]{round(expectancy, 2)}", - fth.red_or_green(round(expectancy_ratio, 2)), - fth.red_or_green( - round(median_win, 2), justify="right" - ), # f"[green]{median_win}", - fth.red_or_green( - round(median_loss, 2), justify="left" - ), # f"[red]{median_loss}", - fth.red_or_green(pcc, justify="right"), - ) - ) - - footer = { - "all_open_profit": fth.red_or_green(round(all_open_profit, 2), justify="right"), - "num_wins_losses": f"[green]{all_wins}/[red]{all_losses}", - "all_total_profit": fth.red_or_green(round(all_profit, 2), justify="right"), - } - - dt = self.query_one("#all-trade-summary-table") - table = fth.dash_trades_summary(row_data, footer) - - worker = get_current_worker() - if not worker.is_cancelled: - self.app.call_from_thread(dt.update, table) - - ats = self.query_one("#dash-all-trade-summary") - ats.loading = False - - @work(group="dash_chart_worker", exclusive=True, thread=True) - def update_cumulative_profit_plot(self): - client_dfs = self.app.client_dfs - - all_cum_data = pd.DataFrame() - if "all_closed" in client_dfs: - all_cum_data = fth.dash_cumulative_profit_plot_data(client_dfs["all_closed"]) - - if "plot_cumprof" in all_cum_data.columns: - chart_container = self.query_one("#dash-cumprof-profit") - cplt = chart_container.plt - dfmt = "Y-m-d" - cplt.date_form(dfmt) - - all_cum_data.index = all_cum_data.index.tz_localize(None) - - dates = cplt.datetimes_to_string(all_cum_data.index) - - cplt.plot( - dates, - all_cum_data["plot_cumprof"].values, - color=self.app.COLOURS.profit_chart_col, - ) - - cplt.ylim( - all_cum_data["plot_cumprof"].min() * 0.99, - all_cum_data["plot_cumprof"].max() * 1.01, - ) - cplt.ylabel("Profit") - - worker = get_current_worker() - if not worker.is_cancelled: - self.app.call_from_thread(chart_container.refresh) - chart_container.loading = False - - @on(Collapsible.Toggled) - def toggle_collapsible(self, event: Collapsible.Toggled) -> None: - event.stop() - collap = event.collapsible - - collap_children = collap.query().filter(".bg-static-default") - - if collap.collapsed is False: - for child in collap_children: - child.loading = True - - if collap.id in self.COLLAP_FUNC_MAP: - getattr(self, self.COLLAP_FUNC_MAP[collap.id])() - else: - for child in collap_children: - child.loading = False - - -class MainBotScreen(Screen): +class MainBotScreen(TimedScreen): timers = {} TAB_FUNC_MAP = { @@ -665,7 +215,8 @@ def select_changed(self, event: Select.Changed) -> None: self.update_chart(bot_id) @work(group="selswitch_workers", exclusive=True, thread=True) - def update_select_options(self, bot_id=None): + @on(ScreenResume) + def update_select_options(self): client_dict = self.app.client_dict options = [] @@ -681,11 +232,8 @@ def update_select_options(self, bot_id=None): sel.set_options(self.client_select_options) - if bot_id is not None: - sel.value = bot_id - else: - if current_selection is not None and current_selection != "Select.BLANK": - sel.value = current_selection + if current_selection is not None and current_selection != "Select.BLANK": + sel.value = current_selection @work(group="tabswitch_workers", exclusive=False, thread=True) def update_tab(self, tab_id, bot_id): @@ -1315,195 +863,3 @@ def _render_sysinfo(self, ftuic): def debug(self, msg): debuglog = self.query_one("#debug-log") debuglog.write(msg) - - -class SettingsScreen(Screen): - timers = {} - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - - with Container(id="settings-above"): - yield Static("FTUI Settings") - yield Button("Save", id="bot-save-settings-button", variant="success") - - with Container(id="parent-container"): - with Container(id="right"): - with Container(id="settings-left"): - yield Label("Server List") - with Container(id="settings-right"): - yield Label("General Config") - - yield Footer() - - def on_mount(self): - self.update_settings(self.app.settings) - - @on(Button.Pressed, "#bot-save-settings-button") - def save_settings_button_pressed(self) -> None: - self.notify( - "Saving of settings is not currently implemented", - title="Not Implemented", - severity="warning", - ) - - def update_settings(self, s): - settings_left = self.query_one("#settings-left") - settings_right = self.query_one("#settings-right") - - for setting in s: - if setting != "yaml": - if isinstance(s[setting], bool): - # output checkbox - c = Checkbox(setting, s[setting]) - settings_right.mount(c) - elif isinstance(s[setting], str): - # output textbox - c = Horizontal(id=f"settings-{setting}") - c.mount(Label(setting), Input(s[setting])) - settings_right.mount(c) - elif isinstance(s[setting], dict): - # nested - print("bloop") - elif isinstance(s[setting], list): - if setting == "servers": - # output server list - c = Container(id=f"settings-{setting}") - for server in s[setting]: - t = Checkbox( - f"{server['name']} [{server['ip']}:{server['port']}]", - server.get("enabled", True), - ) - c.mount(t) - settings_left.mount(c) - - -class HelpScreen(Screen): - timers = {} - - help_file_path = Path(__file__).parent / "md" / "help.md" - - header_str = """ - ███████╗████████╗██╗ ██╗██╗ - ██╔════╝╚══██╔══╝██║ ██║██║ - █████╗ ██║ ██║ ██║██║ - ██╔══╝ ██║ ██║ ██║██║ - ██║ ██║ ╚██████╔╝██║ - ╚═╝ ╚═╝ ╚═════╝ ╚═╝ - """ - - @property - def markdown_viewer(self) -> LinkableMarkdown: - """Get the Markdown widget.""" - return self.query_one(LinkableMarkdown) - - def compose(self) -> ComposeResult: - yield Header(show_clock=True) - - with Container(id="help-above"): - yield Static(self.header_str) - - with Container(id="parent-container"): - with Container(id="right"): - yield LinkableMarkdown() - - yield Footer() - - async def on_mount(self) -> None: - self.markdown_viewer.focus() - try: - await self.markdown_viewer.load(self.help_file_path) - except FileNotFoundError: - msg = f"Unable to load help file: {self.help_file_path!r}" - self.notify( - msg, - title="Error:", - severity="warning", - ) - - -class BasicModal(ModalScreen[int]): - BINDINGS = [ - ("escape", "close_dialog", "Close Dialog"), - ] - - client: ftuic.FTUIClient = None - - def action_close_dialog(self) -> None: - self.app.pop_screen() - - -class DataFrameScreen(BasicModal): - pair: str = "BTC/USDT" - data: pd.DataFrame = pd.DataFrame() - - def compose(self) -> ComposeResult: - main = self.build_dataframe_screen(self.data) - yield Container( - main, - id="dt-dialog", - ) - yield Footer() - - def build_dataframe_screen(self, df) -> DataTable: - dt = DataTable(classes="full-width") - - dt.clear(columns=True) - - dt.add_columns(*df.columns) - for row in df.itertuples(index=False): - dt.add_row(*[str(x) for x in row]) - return dt - - -class TradeInfoScreen(BasicModal): - trade_id: int = "None" - - def compose(self) -> ComposeResult: - trade_info = self.client.get_trade_info(self.trade_id) - main, two = self.build_trade_info(trade_info) - with Container(main, two, id="info-dialog"): - with Container(id="trade-info-footer"): - yield Static("[Esc] to close") - - def build_trade_info(self, trade_info): - main_text = ( - f"[b]Trade Id : {self.trade_id}\n" - f"[b]Pair : {trade_info['pair']}\n" - f"[b]Open Date : {trade_info['open_date']}\n" - f"[b]Entry Tag : {trade_info['enter_tag']}\n" - "[b]Stake : " - f"{trade_info['stake_amount']} " - f"{trade_info['quote_currency']}\n" - f"[b]Amount : {trade_info['amount']}\n" - f"[b]Open Rate : {trade_info['open_rate']}\n" - ) - - if trade_info["close_profit_abs"] is not None: - close_rate = trade_info["close_rate"] - close_date = trade_info["close_date"] - close_profit = f"{trade_info['close_profit_abs']} ({trade_info['close_profit_pct']}%)" - - main_text += ( - f"[b]Close Rate : {close_rate}\n" - f"[b]Close Date : {close_date}\n" - f"[b]Close Profit : {close_profit}\n" - ) - - two_text = ( - f"[b]Stoploss : " - f"{trade_info['stop_loss_pct']} ({trade_info['stop_loss_abs']})\n" - f"[b]Initial Stoploss : " - f"{trade_info['initial_stop_loss_pct']} ({trade_info['initial_stop_loss_abs']})\n" - ) - - # three_text = (7 - # # f"[b]Orders: {self.trade_id}\n" - # ", ".join(list(trade_info.keys())) - # ) - - main = Static(main_text, classes="box", id="trade-info-left") - two = Static(two_text, classes="box", id="trade-info-right") - # three = Static(three_text, classes="box", id="three") - - return main, two diff --git a/ftui/screens/modal_screens.py b/ftui/screens/modal_screens.py new file mode 100644 index 0000000..496e7ec --- /dev/null +++ b/ftui/screens/modal_screens.py @@ -0,0 +1,98 @@ +import pandas as pd +from textual.app import ComposeResult +from textual.containers import Container +from textual.screen import ModalScreen +from textual.widgets import ( + DataTable, + Footer, + Static, +) + +import ftui.ftui_client as ftuic + + +class BasicModal(ModalScreen[int]): + BINDINGS = [ + ("escape", "close_dialog", "Close Dialog"), + ] + + client: ftuic.FTUIClient = None + + def action_close_dialog(self) -> None: + self.app.pop_screen() + + +class DataFrameScreen(BasicModal): + pair: str = "BTC/USDT" + data: pd.DataFrame = pd.DataFrame() + + def compose(self) -> ComposeResult: + main = self.build_dataframe_screen(self.data) + yield Container( + main, + id="dt-dialog", + ) + yield Footer() + + def build_dataframe_screen(self, df) -> DataTable: + dt = DataTable(classes="full-width") + + dt.clear(columns=True) + + dt.add_columns(*df.columns) + for row in df.itertuples(index=False): + dt.add_row(*[str(x) for x in row]) + return dt + + +class TradeInfoScreen(BasicModal): + trade_id: int = "None" + + def compose(self) -> ComposeResult: + trade_info = self.client.get_trade_info(self.trade_id) + main, two = self.build_trade_info(trade_info) + with Container(main, two, id="info-dialog"): + with Container(id="trade-info-footer"): + yield Static("[Esc] to close") + + def build_trade_info(self, trade_info): + main_text = ( + f"[b]Trade Id : {self.trade_id}\n" + f"[b]Pair : {trade_info['pair']}\n" + f"[b]Open Date : {trade_info['open_date']}\n" + f"[b]Entry Tag : {trade_info['enter_tag']}\n" + "[b]Stake : " + f"{trade_info['stake_amount']} " + f"{trade_info['quote_currency']}\n" + f"[b]Amount : {trade_info['amount']}\n" + f"[b]Open Rate : {trade_info['open_rate']}\n" + ) + + if trade_info["close_profit_abs"] is not None: + close_rate = trade_info["close_rate"] + close_date = trade_info["close_date"] + close_profit = f"{trade_info['close_profit_abs']} ({trade_info['close_profit_pct']}%)" + + main_text += ( + f"[b]Close Rate : {close_rate}\n" + f"[b]Close Date : {close_date}\n" + f"[b]Close Profit : {close_profit}\n" + ) + + two_text = ( + f"[b]Stoploss : " + f"{trade_info['stop_loss_pct']} ({trade_info['stop_loss_abs']})\n" + f"[b]Initial Stoploss : " + f"{trade_info['initial_stop_loss_pct']} ({trade_info['initial_stop_loss_abs']})\n" + ) + + # three_text = (7 + # # f"[b]Orders: {self.trade_id}\n" + # ", ".join(list(trade_info.keys())) + # ) + + main = Static(main_text, classes="box", id="trade-info-left") + two = Static(two_text, classes="box", id="trade-info-right") + # three = Static(three_text, classes="box", id="three") + + return main, two diff --git a/ftui/screens/settings_screen.py b/ftui/screens/settings_screen.py new file mode 100644 index 0000000..2d610e8 --- /dev/null +++ b/ftui/screens/settings_screen.py @@ -0,0 +1,78 @@ +from textual import on +from textual.app import ComposeResult +from textual.containers import Container, Horizontal +from textual.events import ScreenResume +from textual.screen import Screen +from textual.widgets import ( + Button, + Checkbox, + Footer, + Header, + Input, + Label, + Static, +) + + +class SettingsScreen(Screen): + def compose(self) -> ComposeResult: + yield Header(show_clock=True) + + with Container(id="settings-above"): + yield Static("FTUI Settings") + yield Button("Save", id="bot-save-settings-button", variant="success") + + with Container(id="parent-container"): + with Container(id="right"): + with Container(id="settings-left"): + yield Label("Server List") + with Container(id="settings-right"): + yield Label("General Config") + + yield Footer() + + #def on_mount(self): + # self.update_settings(self.app.settings) + + @on(ScreenResume) + def on_resume(self): + self.update_settings(self.app.settings) + + @on(Button.Pressed, "#bot-save-settings-button") + def save_settings_button_pressed(self) -> None: + self.notify( + "Saving of settings is not currently implemented", + title="Not Implemented", + severity="warning", + ) + + def update_settings(self, s): + settings_left = self.query_one("#settings-left") + settings_right = self.query_one("#settings-right") + + for setting in s: + if setting != "yaml": + if isinstance(s[setting], bool): + # output checkbox + c = Checkbox(setting, s[setting]) + settings_right.mount(c) + elif isinstance(s[setting], str): + # output textbox + c = Horizontal(id=f"settings-{setting}") + c.mount(Label(setting), Input(s[setting])) + settings_right.mount(c) + elif isinstance(s[setting], dict): + # nested + print("bloop") + elif isinstance(s[setting], list): + if setting == "servers": + # output server list + c = Container(id=f"settings-{setting}") + settings_left.mount(c) + + for server in s[setting]: + t = Checkbox( + f"{server['name']} [{server['ip']}:{server['port']}]", + server.get("enabled", True), + ) + c.mount(t) diff --git a/ftui/widgets/timed_screen.py b/ftui/widgets/timed_screen.py new file mode 100644 index 0000000..7bfa077 --- /dev/null +++ b/ftui/widgets/timed_screen.py @@ -0,0 +1,19 @@ +from textual import on +from textual.events import ScreenResume, ScreenSuspend +from textual.screen import Screen + + +class TimedScreen(Screen): + timers = {} + + @on(ScreenSuspend) + def pause_timers(self): + for ts in self.timers.keys(): + print(f"Pausing {self.id} {ts}") + self.timers[ts].pause() + + @on(ScreenResume) + def resume_timers(self): + for ts in self.timers.keys(): + print(f"Resuming {self.id} {ts}") + self.timers[ts].resume() diff --git a/pyproject.toml b/pyproject.toml index b92c725..783bbc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ 'requests', 'python-rapidjson', 'PyYaml', - 'textual>=0.55.1,<0.77', + 'textual>=0.55.1', 'textual_plotext', 'freqtrade-client', ]