Visual improvements to diverged symbols chart and added log file and help file windows.

This commit is contained in:
Jamie Cash
2021-06-02 16:03:30 +01:00
parent 66cdf47406
commit 1656c6a4d2
6 changed files with 185 additions and 59 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
import os import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
HELP_FILE = fr"{ROOT_DIR}\README.md"
LOG_FILE = fr"{ROOT_DIR}\debug.log"
+25 -8
View File
@@ -1,15 +1,14 @@
import abc import abc
import importlib import importlib
import logging import logging
import sys
import pytz import pytz
import wx import wx
import wx.lib.inspection as ins
import wxconfig as cfg import wxconfig as cfg
from datetime import datetime, timedelta from datetime import datetime, timedelta
import mt5_correlation.gui as gui
from mt5_correlation import correlation as cor from mt5_correlation import correlation as cor
@@ -84,6 +83,14 @@ class CorrelationMDIFrame(wx.MDIParentFrame):
"View diverged symbols.")) "View diverged symbols."))
self.menubar.Append(menu_view, "&View") self.menubar.Append(menu_view, "&View")
# Help menu and items
help_menu = wx.Menu()
self.Bind(wx.EVT_MENU, self.__on_view_log, help_menu.Append(wx.ID_ANY, "View Log", "Show application log."))
self.Bind(wx.EVT_MENU, self.__on_view_help, help_menu.Append(wx.ID_ANY, "Help",
"Show application usage instructions."))
self.menubar.Append(help_menu, "&Help")
# Set menu bar # Set menu bar
self.SetMenuBar(self.menubar) self.SetMenuBar(self.menubar)
@@ -262,6 +269,16 @@ class CorrelationMDIFrame(wx.MDIParentFrame):
frame_class='MDIChildDivergedSymbols', frame_class='MDIChildDivergedSymbols',
raise_if_open=True) raise_if_open=True)
def __on_view_log(self, evt):
FrameManager.open_frame(parent=self, frame_module='mt5_correlation.gui.mdi_child_log',
frame_class='MDIChildLog',
raise_if_open=True)
def __on_view_help(self, evt):
FrameManager.open_frame(parent=self, frame_module='mt5_correlation.gui.mdi_child_help',
frame_class='MDIChildHelp',
raise_if_open=True)
def __refresh(self): def __refresh(self):
""" """
Refresh all open child frames Refresh all open child frames
@@ -272,11 +289,12 @@ class CorrelationMDIFrame(wx.MDIParentFrame):
for child in children: for child in children:
if isinstance(child, CorrelationMDIChild): if isinstance(child, CorrelationMDIChild):
child.refresh() child.refresh()
elif isinstance(child, wx.StatusBar): elif isinstance(child, wx.StatusBar) or isinstance(child, ins.InspectionFrame):
# Ignore # Ignore
pass pass
else: else:
raise Exception(f"MDI Child for application must implement CorrelationMDIChild.") raise Exception(f"MDI Child for application must implement CorrelationMDIChild. MDI Child is "
f"{type(child)}.")
class CorrelationMDIChild(wx.MDIChildFrame): class CorrelationMDIChild(wx.MDIChildFrame):
@@ -305,9 +323,9 @@ class FrameManager:
:param frame_module: A string specifying the module containing the frame class to open or raise :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 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 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 :param kwargs: A dict of parameters to pass to frame constructor. These will also be checked in raise_if_open
determine uniqueness (i.e. If a frame of the same class is already open but its params are different, then to determine uniqueness (i.e. If a frame of the same class is already open but its params are different,
the frame will be opened again with the new params instead of being raised.) then the frame will be opened again with the new params instead of being raised.)
:return: :return:
""" """
@@ -337,4 +355,3 @@ class FrameManager:
clazz(parent=parent, **kwargs).Show(True) clazz(parent=parent, **kwargs).Show(True)
else: else:
opened_instance.Raise() opened_instance.Raise()
+72 -50
View File
@@ -3,7 +3,6 @@ import matplotlib.dates
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib.ticker as mticker import matplotlib.ticker as mticker
import wx import wx
import wxconfig as cfg
import wx.lib.scrolledpanel as scrolled import wx.lib.scrolledpanel as scrolled
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
@@ -23,10 +22,12 @@ class MDIChildDivergedGraph(mdi.CorrelationMDIChild):
__tick_fmt_date = matplotlib.dates.DateFormatter('%d-%b') __tick_fmt_date = matplotlib.dates.DateFormatter('%d-%b')
__tick_fmt_time = matplotlib.dates.DateFormatter('%H:%M:%S') __tick_fmt_time = matplotlib.dates.DateFormatter('%H:%M:%S')
# Fig, axes and canvas # Panel
__fig = None __panel = None
__axs = None
# Graph Canvas and Figure
__canvas = None __canvas = None
__fig = None
# Colors for graph lines # Colors for graph lines
__colours = ['red', 'green', 'blue', 'pink', 'purple', 'black', 'yellow', 'lightblue'] __colours = ['red', 'green', 'blue', 'pink', 'purple', 'black', 'yellow', 'lightblue']
@@ -42,40 +43,18 @@ class MDIChildDivergedGraph(mdi.CorrelationMDIChild):
# Store the symbol # Store the symbol
self.symbol = kwargs['symbol'] self.symbol = kwargs['symbol']
# We will freeze this frame and thaw once constructed to avoid flicker. # Create panel and sizer
self.Freeze() self.__panel = scrolled.ScrolledPanel(self, wx.ID_ANY)
# 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() sizer = wx.BoxSizer()
panel.SetSizer(sizer) self.__panel.SetSizer(sizer)
# Add fig to canvas and canvas to sizer. Thaw window to update # Create figure and canvas. Add canvas to sizer
self.__canvas = FigureCanvas(panel, wx.ID_ANY, self.__fig) self.__fig = plt.Figure()
sizer.Add(self.__canvas, 1, wx.ALL | wx.EXPAND) self.__canvas = FigureCanvas(self.__panel, wx.ID_ANY, self.__fig)
self.Thaw() self.__panel.GetSizer().Add(self.__canvas, 1, wx.ALL | wx.EXPAND)
# Setup scrolling # Setup scrolling
panel.SetupScrolling() self.__panel.SetupScrolling()
# Refresh to show content # Refresh to show content
self.refresh() self.refresh()
@@ -100,7 +79,8 @@ class MDIChildDivergedGraph(mdi.CorrelationMDIChild):
# Get all symbols and remove the base one. We will need to ensure that this is first in the list # 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 = list(filtered_data['Symbol 1'].append(filtered_data['Symbol 2']).drop_duplicates())
other_symbols.remove(self.symbol) if self.symbol in other_symbols:
other_symbols.remove(self.symbol)
# Get the tick data for base symbols and other symbols # Get the tick data for base symbols and other symbols
symbol_tick_data = self.GetMDIParent().cor.get_ticks(self.symbol, cache_only=True) symbol_tick_data = self.GetMDIParent().cor.get_ticks(self.symbol, cache_only=True)
@@ -109,29 +89,71 @@ class MDIChildDivergedGraph(mdi.CorrelationMDIChild):
tick_data = self.GetMDIParent().cor.get_ticks(symbol, cache_only=True) tick_data = self.GetMDIParent().cor.get_ticks(symbol, cache_only=True)
other_tick_data.append(tick_data) other_tick_data.append(tick_data)
# Plot tick data for the base symbol # We will clear the figure and create new axis on every refresh as the shape will change on refresh if symbols
self.__axs.plot(symbol_tick_data['time'], symbol_tick_data['ask'], color=self.__colours[0], label=self.symbol) # change
self.__fig.clear()
axs = self.__fig.add_subplot(111) # Axis to fill canvas
# Plot for the other symbols on new axes # Set title
axs.set_title(f"Price Data for {self.symbol}:{other_symbols}".replace("'", ""))
# 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])
# Store the lines for the legend
lines = []
# 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 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)): for i in range(0, len(other_tick_data)):
new_ax = self.__axs.twinx() other_axis = axs.twinx()
new_ax.plot(other_tick_data[i]['time'], other_tick_data[i]['ask'], label=other_symbols[i],
color=self.__colours[i+1]) # Move spine if we are not on first plot
new_ax.set_ylabel(f"{other_symbols[i]}", color=self.__colours[i+1], labelpad=10) if i > 0:
new_ax.tick_params(axis='y', labelcolor=self.__colours[i+1]) 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 # Ticks, labels and formats. Fixing xticks with FixedLocator but also using MaxNLocator to avoid
# cramped x-labels # cramped x-labels
if len(symbol_tick_data['time']) > 0: if len(symbol_tick_data['time']) > 0:
self.__axs.xaxis.set_major_locator(mticker.MaxNLocator(10)) axs.xaxis.set_major_locator(mticker.MaxNLocator(10))
ticks_loc = self.__axs.get_xticks().tolist() ticks_loc = axs.get_xticks().tolist()
self.__axs.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) axs.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc))
self.__axs.set_xticklabels(ticks_loc) axs.set_xticklabels(ticks_loc)
self.__axs.xaxis.set_major_formatter(self.__tick_fmt_time) axs.xaxis.set_major_formatter(self.__tick_fmt_time)
plt.setp(self.__axs.xaxis.get_majorticklabels(), rotation=45) plt.setp(axs.xaxis.get_majorticklabels(), rotation=45)
# Legend # Legend
#self.__axs.legend([self.symbol, ] + other_symbols) # axs.legend(lines, [line.get_label() for line in lines], loc=0)
# Tight layout
self.__fig.tight_layout()
# Redraw canvas # Redraw canvas
self.__canvas.draw() self.__canvas.draw()
+41
View File
@@ -0,0 +1,41 @@
import definitions
import markdown
import wx
import wx.html
import mt5_correlation.gui.mdi as mdi
class MDIChildHelp(mdi.CorrelationMDIChild):
"""
Shows the README.md file
"""
def __init__(self, parent):
# Super
mdi.CorrelationMDIChild.__init__(self, parent=parent, id=wx.ID_ANY, pos=wx.DefaultPosition, title="Help",
size=wx.Size(width=800, height=-1),
style=wx.DEFAULT_FRAME_STYLE)
# Panel and sizer for help file
panel = wx.Panel(self, wx.ID_ANY)
sizer = wx.BoxSizer()
panel.SetSizer(sizer)
# HtmlWindow
html_widget = wx.html.HtmlWindow(parent=panel, id=wx.ID_ANY, style=wx.html.HW_SCROLLBAR_AUTO | wx.html.HW_NO_SELECTION)
sizer.Add(html_widget, 1, wx.ALL | wx.EXPAND)
# Load the help file, convert markdown to HTML and display. The markdown library doesnt understand the shell
# format so we will remove.
markdown_text = open(definitions.HELP_FILE).read()
markdown_text = markdown_text.replace("```shell", "```")
html = markdown.markdown(markdown_text)
html_widget.SetPage(html)
def refresh(self):
"""
Nothing to do on refresh. Help file doesnt change during outside of development.
:return:
"""
pass
+43
View File
@@ -0,0 +1,43 @@
import definitions
import wx
import wx.html
import mt5_correlation.gui.mdi as mdi
class MDIChildLog(mdi.CorrelationMDIChild):
"""
Shows the debug.log file
"""
__log_window = None # Widget to display log file in
def __init__(self, parent):
# Super
mdi.CorrelationMDIChild.__init__(self, parent=parent, id=wx.ID_ANY, pos=wx.DefaultPosition, title="Log",
size=wx.Size(width=800, height=200),
style=wx.DEFAULT_FRAME_STYLE)
# Panel and sizer for help file
panel = wx.Panel(self, wx.ID_ANY)
sizer = wx.BoxSizer()
panel.SetSizer(sizer)
# Log file window
self.__log_window = wx.TextCtrl(parent=panel, id=wx.ID_ANY, style=wx.HSCROLL | wx.TE_MULTILINE | wx.TE_READONLY)
sizer.Add(self.__log_window, 1, wx.ALL | wx.EXPAND)
# Refresh to populate
self.refresh()
def refresh(self):
"""
Refresh the log file
:return:
"""
# Load the help file
self.__log_window.LoadFile(definitions.LOG_FILE)
# Scroll to bottom
self.__log_window.SetInsertionPoint(-1)
+1
View File
@@ -4,6 +4,7 @@ MetaTrader5==5.0.34
pytz==2021.1 pytz==2021.1
scipy==1.6.0 scipy==1.6.0
logging==0.4.9.6 logging==0.4.9.6
markdown==3.3.4
wxpython==4.1.1 wxpython==4.1.1
mock==4.0.3 mock==4.0.3
numpy==1.20.0 numpy==1.20.0