diff --git a/mt5_correlation/gui/mdi.py b/mt5_correlation/gui/mdi.py index 443a38e..9dd6999 100644 --- a/mt5_correlation/gui/mdi.py +++ b/mt5_correlation/gui/mdi.py @@ -1,11 +1,15 @@ import abc +import importlib import logging +import sys + import pytz import wx import wxconfig as cfg from datetime import datetime, timedelta +import mt5_correlation.gui as gui from mt5_correlation import correlation as cor @@ -249,32 +253,14 @@ class CorrelationMDIFrame(wx.MDIParentFrame): self.SetStatusText(f"Status updated at {self.cor.get_last_calculation():%d-%b %H:%M:%S}.", 1) def __on_view_status(self, evt): - from mt5_correlation.gui.mdi_child_status import MDIChildStatus - - # Only open if not already open. If already open then raise to top. - opened_instance = None - for child in self.GetChildren(): - if isinstance(child, MDIChildStatus): - opened_instance = child - - if opened_instance is None: - MDIChildStatus(parent=self).Show(True) - else: - opened_instance.Raise() + FrameManager.open_frame(parent=self, frame_module='mt5_correlation.gui.mdi_child_status', + frame_class='MDIChildStatus', + raise_if_open=True) def __on_view_diverged(self, evt): - from mt5_correlation.gui.mdi_child_diverged_symbols import MDIChildDivergedSymbols - - # Only open if not already open. If already open then raise to top. - opened_instance = None - for child in self.GetChildren(): - if isinstance(child, MDIChildDivergedSymbols): - opened_instance = child - - if opened_instance is None: - MDIChildDivergedSymbols(parent=self).Show(True) - else: - opened_instance.Raise() + FrameManager.open_frame(parent=self, frame_module='mt5_correlation.gui.mdi_child_diverged_symbols', + frame_class='MDIChildDivergedSymbols', + raise_if_open=True) def __refresh(self): """ @@ -305,3 +291,50 @@ class CorrelationMDIChild(wx.MDIChildFrame): :return: """ raise NotImplementedError + + +class FrameManager: + """ + Manages the opening and raising of MDIChild frames + """ + @staticmethod + def open_frame(parent, frame_module, frame_class, raise_if_open=True, **kwargs): + """ + Opens the frame specified by the frame class + :param parent: The MDIParentFrame to open the child frame into + :param frame_module: A string specifying the module containing the frame class to open or raise + :param frame_class: A string specifying the frame class to open or raise + :param raise_if_open: Whether the frame should raise rather than open if an instance is already open. + :param kwargs: A dict of parameters to pass to frame constructor. These will also be checked in raise_if_open to + determine uniqueness (i.e. If a frame of the same class is already open but its params are different, then + the frame will be opened again with the new params instead of being raised.) + :return: + """ + + # Load the module and class + module = importlib.import_module(frame_module) + clazz = getattr(module, frame_class) + + # Do we have an opened instance + opened_instance = None + for child in parent.GetChildren(): + if isinstance(child, clazz): + # do the args match + match = True + for key in kwargs: + if kwargs[key] != getattr(child, key): + match = False + + # Only open existing instance if args matched + if match: + opened_instance = child + + # If we dont have an opened instance or raise_on_open is False then open new frame, otherwise raise it + if opened_instance is None or raise_if_open is False: + if len(kwargs) == 0: + clazz(parent=parent).Show(True) + else: + clazz(parent=parent, **kwargs).Show(True) + else: + opened_instance.Raise() + diff --git a/mt5_correlation/gui/mdi_child_correlationgraph.py b/mt5_correlation/gui/mdi_child_correlationgraph.py index 07d6819..09f2e20 100644 --- a/mt5_correlation/gui/mdi_child_correlationgraph.py +++ b/mt5_correlation/gui/mdi_child_correlationgraph.py @@ -30,16 +30,16 @@ class MDIChildCorrelationGraph(mdi.CorrelationMDIChild): __axs = None __canvas = None - def __init__(self, parent, symbol1, symbol2): + def __init__(self, parent, **kwargs): # Super wx.MDIChildFrame.__init__(self, parent=parent, id=wx.ID_ANY, - title=f"Correlation Status for {symbol1}:{symbol2}") + title=f"Correlation Status for {kwargs['symbols'][0]}:{kwargs['symbols'][1]}") # Create logger self.__log = logging.getLogger(__name__) # Store the symbols - self.symbols = [symbol1, symbol2] + self.symbols = kwargs['symbols'] # We will freeze this frame and thaw once constructed to avoid flicker. self.Freeze() diff --git a/mt5_correlation/gui/mdi_child_diverged_symbols.py b/mt5_correlation/gui/mdi_child_diverged_symbols.py index a4572ce..0005754 100644 --- a/mt5_correlation/gui/mdi_child_diverged_symbols.py +++ b/mt5_correlation/gui/mdi_child_diverged_symbols.py @@ -56,6 +56,9 @@ class MDIChildDivergedSymbols(mdi.CorrelationMDIChild): self.__grid.SetMinSize((220, 100)) sizer.Add(self.__grid, 1, wx.ALL | wx.EXPAND) + # Bind row doubleclick + self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.__on_doubleckick_row, self.__grid) + # Refresh to populate self.refresh() @@ -94,6 +97,21 @@ class MDIChildDivergedSymbols(mdi.CorrelationMDIChild): # Update row count self.__rows = cur_rows + def __on_doubleckick_row(self, evt): + """ + Open the graphs when a row is doubleclicked. + :param evt: + :return: + """ + row = evt.GetRow() + symbol = self.__grid.GetCellValue(row, COLUMN_SYMBOL) + + mdi.FrameManager.open_frame(parent=self.GetMDIParent(), + frame_module='mt5_correlation.gui.mdi_child_divergedgraph', + frame_class='MDIChildDivergedGraph', + raise_if_open=True, + symbol=symbol) + class _DataTable(wx.grid.GridTableBase): """ diff --git a/mt5_correlation/gui/mdi_child_divergedgraph.py b/mt5_correlation/gui/mdi_child_divergedgraph.py new file mode 100644 index 0000000..86915c9 --- /dev/null +++ b/mt5_correlation/gui/mdi_child_divergedgraph.py @@ -0,0 +1,141 @@ +import logging +import matplotlib.dates +import matplotlib.pyplot as plt +import matplotlib.ticker as mticker +import wx +import wxconfig as cfg +import wx.lib.scrolledpanel as scrolled + +from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas + +import mt5_correlation.gui.mdi as mdi +from mt5_correlation import correlation as cor + + +class MDIChildDivergedGraph(mdi.CorrelationMDIChild): + """ + Shows the graphs for the specified correlation + """ + + symbol = None # Symbol to chart divergence for. Public as we use to check if window for the symbol is already open. + + # Date formats for graphs + __tick_fmt_date = matplotlib.dates.DateFormatter('%d-%b') + __tick_fmt_time = matplotlib.dates.DateFormatter('%H:%M:%S') + + # Fig, axes and canvas + __fig = None + __axs = None + __canvas = None + + # Colors for graph lines + __colours = ['red', 'green', 'blue', 'pink', 'purple', 'black', 'yellow', 'lightblue'] + + def __init__(self, parent, **kwargs): + # Super + wx.MDIChildFrame.__init__(self, parent=parent, id=wx.ID_ANY, + title=f"Divergence Graph for {kwargs['symbol']}") + + # Create logger + self.__log = logging.getLogger(__name__) + + # Store the symbol + self.symbol = kwargs['symbol'] + + # We will freeze this frame and thaw once constructed to avoid flicker. + self.Freeze() + + # Draw the empty graph. We will populate with data in refresh. + + # Create fig and 1 axes. + self.__fig, self.__axs = plt.subplots(1) + + # Set title + self.__axs.set_title(f"Price Data for {self.symbol} vs Previously Correlated Symbols") + + # Set Y Labels and tick colours for symbol. This will be set for other symbols on plot + for i in range(0, 2): + self.__axs.set_ylabel(f"{self.symbol}", color=self.__colours[0], labelpad=10) + self.__axs.tick_params(axis='y', labelcolor=self.__colours[0]) + + # Hack to stop xaxis dropping outside of window + self.__axs.set_xlabel(" ", labelpad=10) + + # Layout with padding between charts + self.__fig.tight_layout(pad=0.5) + + # Create panel and sizer. This will provide scrollbar + panel = scrolled.ScrolledPanel(self, wx.ID_ANY) + sizer = wx.BoxSizer() + panel.SetSizer(sizer) + + # Add fig to canvas and canvas to sizer. Thaw window to update + self.__canvas = FigureCanvas(panel, wx.ID_ANY, self.__fig) + sizer.Add(self.__canvas, 1, wx.ALL | wx.EXPAND) + self.Thaw() + + # Setup scrolling + panel.SetupScrolling() + + # Refresh to show content + self.refresh() + + def refresh(self): + """ + Refresh the graph + :return: + """ + + # Get the symbols that this one has diverged against. + data = self.GetMDIParent().cor.filtered_coefficient_data + filtered_data = data.loc[( + (data['Status'] == cor.STATUS_DIVERGED) | + (data['Status'] == cor.STATUS_DIVERGING) | + (data['Status'] == cor.STATUS_CONVERGING) + ) & + ( + (data['Symbol 1'] == self.symbol) | + (data['Symbol 2'] == self.symbol) + )] + + # Get all symbols and remove the base one. We will need to ensure that this is first in the list + other_symbols = list(filtered_data['Symbol 1'].append(filtered_data['Symbol 2']).drop_duplicates()) + other_symbols.remove(self.symbol) + + # Get the tick data for base symbols and other symbols + symbol_tick_data = self.GetMDIParent().cor.get_ticks(self.symbol, cache_only=True) + other_tick_data = [] + for symbol in other_symbols: + tick_data = self.GetMDIParent().cor.get_ticks(symbol, cache_only=True) + other_tick_data.append(tick_data) + + # Plot tick data for the base symbol + self.__axs.plot(symbol_tick_data['time'], symbol_tick_data['ask'], color=self.__colours[0], label=self.symbol) + + # Plot for the other symbols on new axes + for i in range(0, len(other_tick_data)): + new_ax = self.__axs.twinx() + new_ax.plot(other_tick_data[i]['time'], other_tick_data[i]['ask'], label=other_symbols[i], + color=self.__colours[i+1]) + new_ax.set_ylabel(f"{other_symbols[i]}", color=self.__colours[i+1], labelpad=10) + new_ax.tick_params(axis='y', labelcolor=self.__colours[i+1]) + + # Ticks, labels and formats. Fixing xticks with FixedLocator but also using MaxNLocator to avoid + # cramped x-labels + if len(symbol_tick_data['time']) > 0: + self.__axs.xaxis.set_major_locator(mticker.MaxNLocator(10)) + ticks_loc = self.__axs.get_xticks().tolist() + self.__axs.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) + self.__axs.set_xticklabels(ticks_loc) + self.__axs.xaxis.set_major_formatter(self.__tick_fmt_time) + plt.setp(self.__axs.xaxis.get_majorticklabels(), rotation=45) + + # Legend + #self.__axs.legend([self.symbol, ] + other_symbols) + + # Redraw canvas + self.__canvas.draw() + + def __del__(self): + # Close all plots + plt.close('all') diff --git a/mt5_correlation/gui/mdi_child_status.py b/mt5_correlation/gui/mdi_child_status.py index 8befeac..37731b6 100644 --- a/mt5_correlation/gui/mdi_child_status.py +++ b/mt5_correlation/gui/mdi_child_status.py @@ -125,20 +125,11 @@ class MDIChildStatus(mdi.CorrelationMDIChild): symbol1 = self.__grid.GetCellValue(row, COLUMN_SYMBOL1) symbol2 = self.__grid.GetCellValue(row, COLUMN_SYMBOL2) - from mt5_correlation.gui.mdi_child_correlationgraph import MDIChildCorrelationGraph - - # Check if already open - instance = None - for child in self.GetMDIParent().GetChildren(): - if isinstance(child, MDIChildCorrelationGraph): - if child.symbols[0] == symbol1 and child.symbols[1] == symbol2: - instance = child - - # If already open, raise to top. Otherwise open - if instance is None: - MDIChildCorrelationGraph(parent=self.GetMDIParent(), symbol1=symbol1, symbol2=symbol2).Show(True) - else: - instance.Raise() + mdi.FrameManager.open_frame(parent=self.GetMDIParent(), + frame_module='mt5_correlation.gui.mdi_child_correlationgraph', + frame_class='MDIChildCorrelationGraph', + raise_if_open=True, + symbols=[symbol1, symbol2]) class _DataTable(wx.grid.GridTableBase):