From 0c17c7e5a96de46ac70e205b950cc55a69d78dde Mon Sep 17 00:00:00 2001 From: Jamie Cash Date: Tue, 9 Mar 2021 16:16:30 +0000 Subject: [PATCH] UI improvements --- config.yaml | 4 +- mt5_correlation/correlation.py | 20 +++- mt5_correlation/gui.py | 189 +++++++++++++++++++++------------ 3 files changed, 136 insertions(+), 77 deletions(-) diff --git a/config.yaml b/config.yaml index 1dc01ce..1790599 100644 --- a/config.yaml +++ b/config.yaml @@ -63,8 +63,8 @@ developer: window: x: 37 y: 42 - width: 1512 - height: 975 + width: 1510 + height: 956 style: 541072960 settings_window: x: 354 diff --git a/mt5_correlation/correlation.py b/mt5_correlation/correlation.py index 55b9f45..94f4953 100644 --- a/mt5_correlation/correlation.py +++ b/mt5_correlation/correlation.py @@ -337,6 +337,18 @@ class Correlation: return history + def clear_coefficient_history(self): + """ + Clears the coefficient history for all symbol pairs + :return: + """ + # Create dataframes for coefficient history. + coefficient_history_columns = ['Symbol 1', 'Symbol 2', 'Coefficient', 'Timeframe', 'Date From', 'Date To'] + self.coefficient_history = pd.DataFrame(columns=coefficient_history_columns) + + # Clear tick data + self.__monitor_tick_data = {} + def get_ticks(self, symbol, date_from=None, date_to=None, cache_time=0, cache_only=False): """ Returns the ticks for the specified symbol. Get's from cache if available and not older than cache_timeframe. @@ -539,13 +551,11 @@ class Correlation: 'Timeframe', 'Last Check', 'Last Coefficient'] self.coefficient_data = pd.DataFrame(columns=coefficient_data_columns) - # Create dataframes for coefficient history. - coefficient_history_columns = ['Symbol 1', 'Symbol 2', 'Coefficient', 'Timeframe', 'Date From', 'Date To'] - self.coefficient_history = pd.DataFrame(columns=coefficient_history_columns) + # Clear coefficient history + self.clear_coefficient_history() - # Clear price data and tick data + # Clear price data self.__price_data = None - self.__monitor_tick_data = {} def __update_coefficient_data(self, symbol1, symbol2, coefficient, timeframe, date_from, date_to): """ diff --git a/mt5_correlation/gui.py b/mt5_correlation/gui.py index e9721a7..d6eb60c 100644 --- a/mt5_correlation/gui.py +++ b/mt5_correlation/gui.py @@ -4,6 +4,7 @@ import matplotlib.pyplot as plt from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas import matplotlib.dates import matplotlib +import matplotlib.ticker as mticker from mt5_correlation.correlation import Correlation from mt5_correlation.config import Config, SettingsDialog @@ -53,55 +54,44 @@ class MonitorFrame(wx.Frame): self.__cor = Correlation() self.__cor.monitoring_threshold = self.__config.get("monitor.monitoring_threshold") - # Status bar - self.statusbar = self.CreateStatusBar(1) + # Status bar. 2 fields, one for monitoring status and one for general status. On open, monitoring status is not + # monitoring. SetBackgroundColour will change colour of both. Couldn't find a way to set on single field only. + self.__statusbar = self.CreateStatusBar(2) + self.__statusbar.SetStatusWidths([100, -1]) + self.SetStatusText("Not Monitoring", 0) - # Menu Bar and file menu + # Menu Bar self.menubar = wx.MenuBar() - file_menu = wx.Menu() - # File menu items + # File menu and items + file_menu = wx.Menu() menu_item_open = file_menu.Append(wx.ID_ANY, "Open", "Open correlations file.") menu_item_save = file_menu.Append(wx.ID_ANY, "Save", "Save correlations file.") menu_item_saveas = file_menu.Append(wx.ID_ANY, "Save As", "Save correlations file.") file_menu.AppendSeparator() - menu_item_calculate = file_menu.Append(wx.ID_ANY, "Calculate", "Calculate base coefficients.") - file_menu.AppendSeparator() menu_item_settings = file_menu.Append(wx.ID_ANY, "Settings", "Change application settings.") file_menu.AppendSeparator() menu_item_exit = file_menu.Append(wx.ID_ANY, "Exit", "Close the application") - - # Add file menu and set menu bar self.menubar.Append(file_menu, "File") + + # Coefficient menu and items + coef_menu = wx.Menu() + menu_item_calculate = coef_menu.Append(wx.ID_ANY, "Calculate", "Calculate base coefficients.") + self.__menu_item_monitor = coef_menu.Append(wx.ID_ANY, "Monitor", "Monitor correlated pairs for changes to " + "coefficient.", kind=wx.ITEM_CHECK) + coef_menu.AppendSeparator() + menu_item_clear = coef_menu.Append(wx.ID_ANY, "Clear", "Clear coefficient and price history.") + self.menubar.Append(coef_menu, "Coefficient") + + # Set menu bar self.SetMenuBar(self.menubar) - # Main window. We want 2 horizontal sections, the grid showing correlations and a graph. In the correlations - # section, we want 2 vertical sections, the monitor toggle and the correlations grid. For the toggle we want 2 - # sections, a label and a toggle. - # --------------------------------------------------------------- - # |label | toggle | | - # |-----------------------| | - # | | | - # |Correlations Grid | Graphs | - # | | | - # | | | - # | | | - # | | | - # ---------------------------------------------------------------- + # Main window. We want 2 horizontal sections, the grid showing correlations and a graph. panel = wx.Panel(self, wx.ID_ANY) - toggle_sizer = wx.BoxSizer(wx.HORIZONTAL) # Label and toggle - correlations_sizer = wx.BoxSizer(wx.VERTICAL) # Toggle sizer and correlations grid + correlations_sizer = wx.BoxSizer(wx.VERTICAL) # Correlations grid self.__main_sizer = wx.BoxSizer(wx.HORIZONTAL) # Correlations sizer and graphs panel panel.SetSizer(self.__main_sizer) - # Create the label and toggle, populate the toggle sizer and add the toggle sizer to the correlations sizer - monitor_toggle_label = wx.StaticText(panel, id=wx.ID_ANY, label="Monitoring") - toggle_sizer.Add(monitor_toggle_label, 0, wx.ALL, 1) - self.monitor_toggle = wx.ToggleButton(panel, wx.ID_ANY, label="Off") - self.monitor_toggle.SetBackgroundColour(wx.RED) - toggle_sizer.Add(self.monitor_toggle, 0, wx.ALL, 1) - correlations_sizer.Add(toggle_sizer, 0, wx.ALL, 1) - # Create the correlations grid. This is a data table using pandas dataframe for underlying data. Add the # correlations_grid to the correlations sizer. self.table = DataTable(self.__cor.filtered_coefficient_data) @@ -140,9 +130,6 @@ class MonitorFrame(wx.Frame): # Set up timer to refresh grid self.timer = wx.Timer(self) - # Bind monitor button - self.monitor_toggle.Bind(wx.EVT_TOGGLEBUTTON, self.monitor) - # Bind timer self.Bind(wx.EVT_TIMER, self.__timer_event, self.timer) @@ -152,6 +139,8 @@ class MonitorFrame(wx.Frame): self.Bind(wx.EVT_MENU, self.save_file_as, menu_item_saveas) self.Bind(wx.EVT_MENU, self.calculate_coefficients, menu_item_calculate) self.Bind(wx.EVT_MENU, self.open_settings, menu_item_settings) + self.Bind(wx.EVT_MENU, self.__monitor, self.__menu_item_monitor) + self.Bind(wx.EVT_MENU, self.__clear_history, menu_item_clear) self.Bind(wx.EVT_MENU, self.quit, menu_item_exit) # Bind row select @@ -170,23 +159,23 @@ class MonitorFrame(wx.Frame): # Load the file chosen by the user. self.__opened_filename = fileDialog.GetPath() - self.SetStatusText(f"Loading file {self.__opened_filename}.") + self.SetStatusText(f"Loading file {self.__opened_filename}.", 1) self.__cor.load(self.__opened_filename) # Refresh data in grid self.refresh_grid() - self.SetStatusText(f"File {self.__opened_filename} loaded.") + self.SetStatusText(f"File {self.__opened_filename} loaded.", 1) def save_file(self, event): - self.SetStatusText(f"Saving file as {self.__opened_filename}") + self.SetStatusText(f"Saving file as {self.__opened_filename}", 1) if self.__opened_filename is None: self.save_file_as(event) else: self.__cor.save(self.__opened_filename) - self.SetStatusText(f"File saved as {self.__opened_filename}") + self.SetStatusText(f"File saved as {self.__opened_filename}", 1) def save_file_as(self, event): with wx.FileDialog(self, "Save Coefficients file", wildcard="cpd (*.cpd)|*.cpd", @@ -195,12 +184,12 @@ class MonitorFrame(wx.Frame): return # the user changed their mind # Save the file and price data file, changing opened filename so next save writes to new file - self.SetStatusText(f"Saving file as {self.__opened_filename}") + self.SetStatusText(f"Saving file as {self.__opened_filename}", 1) self.__opened_filename = fileDialog.GetPath() self.__cor.save(self.__opened_filename) - self.SetStatusText(f"File saved as {self.__opened_filename}") + self.SetStatusText(f"File saved as {self.__opened_filename}", 1) def calculate_coefficients(self, event): # set time zone to UTC to avoid local offset issues, and get from and to dates (a week ago to today) @@ -209,14 +198,14 @@ class MonitorFrame(wx.Frame): utc_from = utc_to - timedelta(days=self.__config.get('calculate.from.days')) # Calculate - self.SetStatusText("Calculating coefficients.") + self.SetStatusText("Calculating coefficients.", 1) self.__cor.calculate(date_from=utc_from, date_to=utc_to, timeframe=self.__config.get('calculate.timeframe'), min_prices=self.__config.get('calculate.min_prices'), max_set_size_diff_pct=self.__config.get('calculate.max_set_size_diff_pct'), overlap_pct=self.__config.get('calculate.overlap_pct'), max_p_value=self.__config.get('calculate.max_p_value')) - self.SetStatusText("") + self.SetStatusText("", 1) # Show calculated data self.refresh_grid() @@ -270,13 +259,13 @@ class MonitorFrame(wx.Frame): # Update row count self.__rows = cur_rows - def monitor(self, event): + def __monitor(self, event): # Check state of toggle button. If on, then start monitoring, else stop - if self.monitor_toggle.GetValue(): - self.__log.info("Starting monitoring.") - self.monitor_toggle.SetBackgroundColour(wx.GREEN) - self.monitor_toggle.SetLabelText("On") - self.SetStatusText("Monitoring for changes to coefficients.") + if self.__menu_item_monitor.IsChecked(): + self.__log.info("Starting monitoring for changes to coefficients.") + self.SetStatusText("Monitoring", 0) + self.__statusbar.SetBackgroundColour('green') + self.__statusbar.Refresh() self.timer.Start(self.__config.get('monitor.interval')*1000) @@ -295,9 +284,9 @@ class MonitorFrame(wx.Frame): filename=filename) else: self.__log.info("Stopping monitoring.") - self.monitor_toggle.SetBackgroundColour(wx.RED) - self.monitor_toggle.SetLabelText("Off") - self.SetStatusText("Monitoring stopped.") + self.SetStatusText("Not Monitoring", 0) + self.__statusbar.SetBackgroundColour('lightgray') + self.__statusbar.Refresh() self.timer.Stop() self.__cor.stop_monitor() @@ -445,6 +434,20 @@ class MonitorFrame(wx.Frame): if len(self.__selected_correlation) == 2: self.show_graph(symbol1=self.__selected_correlation[0], symbol2=self.__selected_correlation[1]) + def __clear_history(self, event): + """ + Clears the calculated coefficient history and associated price data + :param event: + :return: + """ + # Clear the history + self.__cor.clear_coefficient_history() + + # Reload graph if we have a coefficient selected + self.__log.info("History cleared. Reloading graph.") + if len(self.__selected_correlation) == 2: + self.show_graph(symbol1=self.__selected_correlation[0], symbol2=self.__selected_correlation[1]) + class DataTable(wx.grid.GridTableBase): """ @@ -545,29 +548,39 @@ class GraphPanel(wx.Panel): :return: """ + # Check what data we have available + price_data_available = prices is not None and len(prices) == 2 and \ + prices[0] is not None and prices[1] is not None + tick_data_available = ticks is not None and len(ticks) == 2 and ticks[0] is not None and ticks[1] is not None + history_data_available = history is not None and len(history) > 0 + symbols_selected = symbols is not None and len(symbols) == 2 + # Get all plots for history. History can contain multiple plots for different timeframes. They will all be # plotted on the same chart. times = [] coefficients = [] - for hist in history: - times.append(hist['Date To']) - coefficients.append(hist['Coefficient']) + if history_data_available: + for hist in history: + times.append(hist['Date To']) + coefficients.append(hist['Coefficient']) # Clear. We will need to redraw for ax in self.__axes: ax.clear() - if symbols is not None and len(symbols) == 2: + if symbols_selected: # Axis ranges - default_range = [datetime.now() - timedelta(days=1), datetime.now()] - price_chart_date_range = default_range # We need a default here if no data is available - if prices is not None and len(prices) == 0 and prices[0] is not None and prices[1] is not None: + if price_data_available: price_chart_date_range = [min(min(prices[0]['time']), min(prices[1]['time'])), max(max(prices[0]['time']), max(prices[1]['time']))] - tick_chart_date_range = default_range # We need a default here if no data is available - if ticks is not None and len(ticks) == 0 and ticks[0] is not None and ticks[1] is not None: + else: + price_chart_date_range = [datetime.now() - timedelta(days=1), datetime.now()] + + if tick_data_available: tick_chart_date_range = [min(min(ticks[0]['time']), min(ticks[1]['time'])), max(max(ticks[0]['time']), max(ticks[1]['time']))] + else: + tick_chart_date_range = [datetime.now() - timedelta(days=1/48), datetime.now()] # Chart config titles = [f"Base Coefficient Price Data for {symbols[0]}", f"Base Coefficient Price Data for {symbols[1]}", @@ -575,15 +588,40 @@ class GraphPanel(wx.Panel): f"Coefficient History for {symbols[0]}:{symbols[1]}"] xlims = [price_chart_date_range, price_chart_date_range, tick_chart_date_range, tick_chart_date_range, None] ylims = [None, None, None, None, [-1, 1]] + + # Axis labels xlabels = [None, None, None, None, None] ylabels = ['Price', 'Price', 'Price', 'Price', 'Coefficient'] - tick_labels = [[], prices[1]['time'], [], ticks[1]['time'], times[0]] + + # Tick labels and formats + tick_labels = [[], + prices[1]['time'] if price_data_available else [], + [], + ticks[1]['time'] if tick_data_available else [], + times[0].array if history_data_available else []] mtick_fmts = [None, self.__tick_fmt_date, None, self.__tick_fmt_time, self.__tick_fmt_time] mtick_rot = [0, 45, 0, 45, 45] - xdata = [prices[0]['time'], prices[1]['time'], ticks[0]['time'], ticks[1]['time'], times] - ydata = [prices[0]['close'], prices[1]['close'], ticks[0]['ask'], ticks[1]['ask'], coefficients] + + # Chart data + xdata = [prices[0]['time'] if price_data_available else [], + prices[1]['time'] if price_data_available else [], + ticks[0]['time'] if tick_data_available else [], + ticks[1]['time'] if tick_data_available else [], + times if history_data_available else []] + + ydata = [prices[0]['close'] if price_data_available else [], + prices[1]['close'] if price_data_available else [], + ticks[0]['ask'] if tick_data_available else [], + ticks[1]['ask'] if tick_data_available else [], + coefficients if history_data_available else []] + + # Chart types types = ['plot', 'plot', 'plot', 'plot', 'scatter'] + # Legends + legends = [None, None, None, None, [f"{Config().get('monitor.calculate_from.short.minutes')} Minutes", + f"{Config().get('monitor.calculate_from.long.minutes')} Minutes"]] + # Draw 5 charts for index in range(0, len(self.__axes)): # Titles and axis labels @@ -598,12 +636,6 @@ class GraphPanel(wx.Panel): if ylims[index] is not None: self.__axes[index].set_ylim(ylims[index]) - # Tick labels and formats - self.__axes[index].xaxis.set_ticklabels(tick_labels[index]) - if mtick_fmts[index] is not None: - self.__axes[index].xaxis.set_major_formatter(mtick_fmts[index]) - plt.setp(self.__axes[index].xaxis.get_majorticklabels(), rotation=mtick_rot[index]) - # Remove typ and right boarders self.__axes[index].spines["top"].set_visible(False) self.__axes[index].spines["right"].set_visible(False) @@ -618,6 +650,23 @@ class GraphPanel(wx.Panel): elif types[index] == 'scatter': self.__axes[index].scatter(xdata_list[data_index], ydata_list[data_index], s=1) + # Ticks, labels and formats. Fixing xticks with FixedLocator but also using MaxNLocator to avoid + # cramped x-labels + if len(tick_labels[index]) > 0: + self.__axes[index].xaxis.set_major_locator(mticker.MaxNLocator(10)) + ticks_loc = self.__axes[index].get_xticks().tolist() + self.__axes[index].xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) + self.__axes[index].set_xticklabels(ticks_loc) + if mtick_fmts[index] is not None: + self.__axes[index].xaxis.set_major_formatter(mtick_fmts[index]) + plt.setp(self.__axes[index].xaxis.get_majorticklabels(), rotation=mtick_rot[index]) + else: + self.__axes[index].set_xticklabels([]) + + # Legends + if legends[index] is not None: + self.__axes[index].legend(legends[index]) + # Layout with padding between charts self.__fig.tight_layout(pad=0.5)