Visual improvements to diverged symbols chart and added log file and help file windows.
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user