From 7d97b78f8c402342d6a24c2ff8a550b520629d01 Mon Sep 17 00:00:00 2001 From: Jamie Cash Date: Fri, 4 Jun 2021 17:40:44 +0100 Subject: [PATCH] Improved divergence graph. --- config.yaml | 10 +- configmeta.yaml | 4 + mt5_correlation/gui/mdi.py | 4 +- .../gui/mdi_child_divergedgraph.py | 197 ++++++++++-------- requirements.txt | 2 +- 5 files changed, 125 insertions(+), 92 deletions(-) diff --git a/config.yaml b/config.yaml index efd0adc..0355ad5 100644 --- a/config.yaml +++ b/config.yaml @@ -8,7 +8,7 @@ calculate: overlap_pct: 90 max_p_value: 0.05 monitor: - interval: 20 + interval: 10 calculations: long: from: 30 @@ -74,10 +74,10 @@ charts: developer: inspection: true window: - x: -8 - y: -8 - width: 1636 - height: 1056 + x: -4 + y: 1 + width: 1626 + height: 1043 style: 541072960 settings_window: x: 354 diff --git a/configmeta.yaml b/configmeta.yaml index c90dd32..ea6298c 100644 --- a/configmeta.yaml +++ b/configmeta.yaml @@ -93,6 +93,10 @@ monitor: autosave: __label: Auto Save __helptext: Whether to auto save after every monitoring event. If a file was opened or has been saved, then the data will be saved to this file, otherwise the data will be saved to a file named autosave.cpd. +charts: + colormap: + __label: Color Map + __helptext: The matplotlib color pallet to use for plotting graphs. A list of pallets is available at https://matplotlib.org/stable/tutorials/colors/colormaps.html developer: inspection: __label: Inspection diff --git a/mt5_correlation/gui/mdi.py b/mt5_correlation/gui/mdi.py index 89aeaff..899c6be 100644 --- a/mt5_correlation/gui/mdi.py +++ b/mt5_correlation/gui/mdi.py @@ -5,6 +5,7 @@ import logging import pytz import wx import wx.lib.inspection as ins +import wxconfig import wxconfig as cfg from datetime import datetime, timedelta @@ -289,7 +290,8 @@ class CorrelationMDIFrame(wx.MDIParentFrame): for child in children: if isinstance(child, CorrelationMDIChild): child.refresh() - elif isinstance(child, wx.StatusBar) or isinstance(child, ins.InspectionFrame): + elif isinstance(child, wx.StatusBar) or isinstance(child, ins.InspectionFrame) or \ + isinstance(child, wxconfig.SettingsDialog): # Ignore pass else: diff --git a/mt5_correlation/gui/mdi_child_divergedgraph.py b/mt5_correlation/gui/mdi_child_divergedgraph.py index d3c24e8..95c48ae 100644 --- a/mt5_correlation/gui/mdi_child_divergedgraph.py +++ b/mt5_correlation/gui/mdi_child_divergedgraph.py @@ -1,15 +1,14 @@ import logging import matplotlib.dates -import matplotlib.pylab as pl import matplotlib.pyplot as plt import matplotlib.ticker as mticker -import numpy as mp -import numpy as np import wx import wx.lib.scrolledpanel as scrolled import wxconfig as cfg from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas +from mpl_toolkits.axes_grid1 import host_subplot +from mpl_toolkits import axisartist import mt5_correlation.gui.mdi as mdi from mt5_correlation import correlation as cor @@ -22,20 +21,23 @@ class MDIChildDivergedGraph(mdi.CorrelationMDIChild): symbol = None # Symbol to chart divergence for. Public as we use to check if window for the symbol is already open. + # Logger + __log = None + # Date formats for graphs __tick_fmt_date = matplotlib.dates.DateFormatter('%d-%b') __tick_fmt_time = matplotlib.dates.DateFormatter('%H:%M:%S') - # Panel - __panel = None - - # Graph Canvas and Figure + # Graph Canvas __canvas = None - __fig = None # Colors for graph lines __colours = matplotlib.cm.get_cmap(cfg.Config().get("charts.colormap")).colors + # We will store the other symbols last plotted. This will save us rebuilding teh figure if teh symbols haven't + # changed. + __other_symbols = None + def __init__(self, parent, **kwargs): # Super wx.MDIChildFrame.__init__(self, parent=parent, id=wx.ID_ANY, @@ -48,17 +50,14 @@ class MDIChildDivergedGraph(mdi.CorrelationMDIChild): self.symbol = kwargs['symbol'] # Create panel and sizer - self.__panel = scrolled.ScrolledPanel(self, wx.ID_ANY) + panel = scrolled.ScrolledPanel(self, wx.ID_ANY) sizer = wx.BoxSizer() - self.__panel.SetSizer(sizer) + panel.SetSizer(sizer) # Create figure and canvas. Add canvas to sizer - self.__fig = plt.Figure() - self.__canvas = FigureCanvas(self.__panel, wx.ID_ANY, self.__fig) - self.__panel.GetSizer().Add(self.__canvas, 1, wx.ALL | wx.EXPAND) - - # Setup scrolling - self.__panel.SetupScrolling() + self.__canvas = FigureCanvas(panel, wx.ID_ANY, plt.figure()) + panel.GetSizer().Add(self.__canvas, 1, wx.ALL | wx.EXPAND) + panel.SetupScrolling() # Refresh to show content self.refresh() @@ -69,6 +68,41 @@ class MDIChildDivergedGraph(mdi.CorrelationMDIChild): :return: """ + # Get tick data for base symbol + symbol_tick_data = self.GetMDIParent().cor.get_ticks(self.symbol, cache_only=True) + + # Get the other symbols and their tick data + other_symbols_data = self.__get_other_symbols_data() + + # Delete all axes from the figure. They will need to be recreated as the symbols may be different + for axes in self.__canvas.figure.axes: + axes.remove() + + # Plot for all other symbols + num_subplots = 1 if len(other_symbols_data.keys()) == 0 else len(other_symbols_data.keys()) + plotnum = 1 + axs = [] # Store the axes for sharing x axis + for other_symbol in other_symbols_data: + axs.append(self.__canvas.figure.add_subplot(num_subplots, 1, plotnum)) + self.__plot(axes=axs[-1], base_symbol=self.symbol, other_symbol=other_symbol, + base_symbol_data=symbol_tick_data, other_symbol_data=other_symbols_data[other_symbol]) + + # Next plot + plotnum += 1 + + # Share x axis of the last axes with all the others + self.__share_xaxis(axs) + + # Redraw canvas + self.__canvas.figure.tight_layout(pad=0.5) + self.__canvas.draw() + + def __get_other_symbols_data(self): + """ + Gets the data required for the graphs + :param self: + :return: dict of other symbols their tick data + """ # Get the symbols that this one has diverged against. data = self.GetMDIParent().cor.filtered_coefficient_data filtered_data = data.loc[( @@ -77,90 +111,83 @@ class MDIChildDivergedGraph(mdi.CorrelationMDIChild): (data['Status'] == cor.STATUS_CONVERGING) ) & ( - (data['Symbol 1'] == self.symbol) | - (data['Symbol 2'] == self.symbol) - )] + (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()) if self.symbol in other_symbols: 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 = [] + # Get the tick data other symbols and add to dict + 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) + other_tick_data[symbol] = tick_data - # We will clear the figure and create new axis on every refresh as the shape will change on refresh if symbols - # change - self.__fig.clear() - axs = self.__fig.add_subplot(111) # Axis to fill canvas + return other_tick_data - # Set title - axs.set_title(f"Price Data for {self.symbol}:{other_symbols}".replace("'", "")) + def __plot(self, axes, base_symbol, other_symbol, base_symbol_data, other_symbol_data): + """ + Plots the data on the axes + :param axes: The subplot to plot onto + :param base_symbol + :param other_symbol: + """ + # Create the other axes. Will need an axes for the base symbol data and another for the other symbol data + other_axes = axes.twinx() - # Set Y Labels and tick colours for symbol. This will be set for other symbols on plot - for i in range(0, 2): - axs.set_ylabel(f"{self.symbol}", color=self.__colours[0], labelpad=10) - axs.tick_params(axis='y', labelcolor=self.__colours[0]) + # Set plot title and axis labels + axes.set_title(f"Tick Data for {base_symbol}:{other_symbol}") + axes.set_ylabel(base_symbol, color=self.__colours[0], labelpad=10) + other_axes.set_ylabel(other_symbol, color=self.__colours[1], labelpad=10) - # Store the lines for the legend - lines = [] + # Set the tick and axis colors + self.__set_axes_color(axes, self.__colours[0], 'left') + self.__set_axes_color(other_axes, self.__colours[1], 'right') - # Plot tick data for the base symbol. Store line and label. - line = axs.plot(symbol_tick_data['time'], symbol_tick_data['ask'], color=self.__colours[0], - label=self.symbol)[0] - lines.append(line) + # Plot both lines + axes.plot(base_symbol_data['time'], base_symbol_data['ask'], color=self.__colours[0]) + other_axes.plot(other_symbol_data['time'], other_symbol_data['ask'], color=self.__colours[1]) - # Plot for the other symbols on new axes. - spinepos = 0 # Position of spine. Starting at 2nd plot, will be increased for every plot. - for i in range(0, len(other_tick_data)): - other_axis = axs.twinx() + @staticmethod + def __set_axes_color(axes, color, axis_loc='right'): + """ + Set the color for the axes, including axis line, ticks and tick labels + :param axes: The axes to set color for. + :param color: The color to set to + :param axis_loc: The location of the axis, label and ticks. Either left for base symbol or right for others + :return: + """ + # Change the color for the axis line, ticks and tick labels + axes.spines[axis_loc].set_color(color) + axes.tick_params(axis='y', colors=color) - # Move spine if we are not on first plot - if i > 0: - spinepos += 1.15 - other_axis.spines['right'].set_position(("axes", spinepos)) - - # The frame is off, so line of detached spine is invisible. Activate frame and then make patch and - # spines invisible - other_axis.set_frame_on(True) - other_axis.patch.set_visible(False) - for sp in other_axis.spines.values(): - sp.set_visible(False) - - # Show the right spline - other_axis.spines['right'].set_visible(True) - - # Plot. We will use a lighter line for the other symbols. Store line. - line = other_axis.plot(other_tick_data[i]['time'], other_tick_data[i]['ask'], color=self.__colours[i + 1], - label=other_symbols[i], linestyle='solid', linewidth=0.5)[0] - lines.append(line) - - # Set y label, label colour and tick colour - other_axis.set_ylabel(f"{other_symbols[i]}", color=self.__colours[i + 1], labelpad=10) - other_axis.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: - axs.xaxis.set_major_locator(mticker.MaxNLocator(10)) - ticks_loc = axs.get_xticks().tolist() - axs.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) - axs.set_xticklabels(ticks_loc) - axs.xaxis.set_major_formatter(self.__tick_fmt_time) - plt.setp(axs.xaxis.get_majorticklabels(), rotation=45) - - # Legend - # axs.legend(lines, [line.get_label() for line in lines], loc=0) - - # Tight layout - self.__fig.tight_layout() - - # Redraw canvas - self.__canvas.draw() + def __share_xaxis(self, axs): + """ + Share the xaxis of the last axes with all other axes. Remove axis tick labels for all but the last. Format axis + tick labels for the last. + :param axs: + :return: + """ + if len(axs) > 0: + last_ax = axs[-1] + for ax in axs: + # If we are not on the last one, share and hide tick labels. If we are on the last one, format tick + # labels. + if ax != last_ax: + ax.sharex(last_ax) + plt.setp(ax.xaxis.get_majorticklabels(), visible=False) + else: + # Ticks, labels and formats. Fixing xticks with FixedLocator but also using MaxNLocator to avoid + # cramped x-labels + ax.xaxis.set_major_locator(mticker.MaxNLocator(10)) + ticks_loc = ax.get_xticks().tolist() + ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) + ax.set_xticklabels(ticks_loc) + ax.xaxis.set_major_formatter(self.__tick_fmt_time) + plt.setp(ax.xaxis.get_majorticklabels(), rotation=45) def __del__(self): # Close all plots diff --git a/requirements.txt b/requirements.txt index 3119096..ce07579 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pandas==1.2.1 -matplotlib==3.3.4 +matplotlib==3.4.2 MetaTrader5==5.0.34 pytz==2021.1 scipy==1.6.0