Converted to MDI

This commit is contained in:
Jamie Cash
2021-05-28 17:34:28 +01:00
parent c977e3ac1c
commit 029e86d358
7 changed files with 708 additions and 711 deletions
+1
View File
@@ -0,0 +1 @@
from mt5_correlation.gui.mdi import CorrelationMDIFrame
+291
View File
@@ -0,0 +1,291 @@
import abc
import logging
import pytz
import wx
import wxconfig as cfg
from datetime import datetime, timedelta
from mt5_correlation import correlation as cor
class CorrelationMDIFrame(wx.MDIParentFrame):
"""
The MDI Frame window for the correlation monitoring application
"""
# The correlation instance that calculates coefficients and monitors for divergence. Needs to be accessible to
# child frames.
cor = None
__opened_filename = None # So we can save to same file as we opened
__log = None # The logger
__menu_item_monitor = None # We need to store this menu item so that we can check if it is checked or not.
def __init__(self):
# Super
wx.MDIParentFrame.__init__(self, parent=None, id=wx.ID_ANY, title="Divergence Monitor",
pos=wx.Point(x=cfg.Config().get('window.x'), y=cfg.Config().get('window.y')),
size=wx.Size(width=cfg.Config().get('window.width'),
height=cfg.Config().get('window.height')),
style=cfg.Config().get('window.style'))
# Create logger
self.__log = logging.getLogger(__name__)
# Create correlation instance to maintain state of calculated coefficients. Set params from config
self.cor = cor.Correlation(monitoring_threshold=cfg.Config().get("monitor.monitoring_threshold"),
divergence_threshold=cfg.Config().get("monitor.divergence_threshold"),
monitor_inverse=cfg.Config().get("monitor.monitor_inverse"))
# 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)
# Create menu bar and bind menu items to methods
self.menubar = wx.MenuBar()
# File menu and items
menu_file = wx.Menu()
self.Bind(wx.EVT_MENU, self.__on_open_file, menu_file.Append(wx.ID_ANY, "&Open", "Open correlations file."))
self.Bind(wx.EVT_MENU, self.__on_save_file, menu_file.Append(wx.ID_ANY, "Save", "Save correlations file."))
self.Bind(wx.EVT_MENU, self.__on_save_file_as,
menu_file.Append(wx.ID_ANY, "Save As", "Save correlations file."))
menu_file.AppendSeparator()
self.Bind(wx.EVT_MENU, self.__on_open_settings,
menu_file.Append(wx.ID_ANY, "Settings", "Change application settings."))
menu_file.AppendSeparator()
self.Bind(wx.EVT_MENU, self.__on_exit, menu_file.Append(wx.ID_ANY, "Exit", "Close the application"))
self.menubar.Append(menu_file, "&File")
# Coefficient menu and items
menu_coef = wx.Menu()
self.Bind(wx.EVT_MENU, self.__on_calculate,
menu_coef.Append(wx.ID_ANY, "Calculate", "Calculate base coefficients."))
self.__menu_item_monitor = menu_coef.Append(wx.ID_ANY, "Monitor",
"Monitor correlated pairs for changes to coefficient.",
kind=wx.ITEM_CHECK)
self.Bind(wx.EVT_MENU, self.__on_monitor, self.__menu_item_monitor)
menu_coef.AppendSeparator()
self.Bind(wx.EVT_MENU, self.__on_clear,
menu_coef.Append(wx.ID_ANY, "Clear", "Clear coefficient and price history."))
self.menubar.Append(menu_coef, "Coefficient")
# View menu and items
menu_view = wx.Menu()
self.Bind(wx.EVT_MENU, self.__on_view_status, menu_view.Append(wx.ID_ANY, "Status",
"View status of correlations."))
self.menubar.Append(menu_view, "&View")
# Set menu bar
self.SetMenuBar(self.menubar)
# Set up timer to refresh
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.__on_timer, self.timer)
# Bind window close event
self.Bind(wx.EVT_CLOSE, self.__on_close, self)
def __on_close(self, event):
"""
Window closing. Save coefficients and stop monitoring.
:param event:
:return:
"""
# Save pos and size
x, y = self.GetPosition()
width, height = self.GetSize()
cfg.Config().set('window.x', x)
cfg.Config().set('window.y', y)
cfg.Config().set('window.width', width)
cfg.Config().set('window.height', height)
# Style
style = self.GetWindowStyle()
cfg.Config().set('window.style', style)
cfg.Config().save()
# Stop monitoring
self.cor.stop_monitor()
# End
event.Skip()
def __on_open_file(self, evt):
with wx.FileDialog(self, "Open Coefficients file", wildcard="cpd (*.cpd)|*.cpd",
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return # the user changed their mind
# Load the file chosen by the user.
self.__opened_filename = fileDialog.GetPath()
self.SetStatusText(f"Loading file {self.__opened_filename}.", 1)
self.cor.load(self.__opened_filename)
# Show calculated data and refresh all opened frames
self.__on_view_status(evt)
self.__refresh()
self.SetStatusText(f"File {self.__opened_filename} loaded.", 1)
def __on_save_file(self, evt):
self.SetStatusText(f"Saving file as {self.__opened_filename}", 1)
if self.__opened_filename is None:
self.__on_save_file_as(evt)
else:
self.cor.save(self.__opened_filename)
self.SetStatusText(f"File saved as {self.__opened_filename}", 1)
def __on_save_file_as(self, evt):
with wx.FileDialog(self, "Save Coefficients file", wildcard="cpd (*.cpd)|*.cpd",
style=wx.FD_SAVE) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
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}", 1)
self.__opened_filename = fileDialog.GetPath()
self.cor.save(self.__opened_filename)
self.SetStatusText(f"File saved as {self.__opened_filename}", 1)
def __on_open_settings(self, evt):
settings_dialog = cfg.SettingsDialog(parent=self, exclude=['window'])
res = settings_dialog.ShowModal()
if res == wx.ID_OK:
# Stop the monitor
self.cor.stop_monitor()
# Build calculation params and restart the monitor
calculation_params = [cfg.Config().get('monitor.calculations.long'),
cfg.Config().get('monitor.calculations.medium'),
cfg.Config().get('monitor.calculations.short')]
self.cor.start_monitor(interval=cfg.Config().get('monitor.interval'),
calculation_params=calculation_params,
cache_time=cfg.Config().get('monitor.tick_cache_time'),
autosave=cfg.Config().get('monitor.autosave'),
filename=self.__opened_filename)
# Refresh all open child frames
self.__refresh()
def __on_exit(self, evt):
# Close
self.Close()
def __on_calculate(self, evt):
# set time zone to UTC to avoid local offset issues, and get from and to dates (a week ago to today)
timezone = pytz.timezone("Etc/UTC")
utc_to = datetime.now(tz=timezone)
utc_from = utc_to - timedelta(days=cfg.Config().get('calculate.from.days'))
# Calculate
self.SetStatusText("Calculating coefficients.", 1)
self.cor.calculate(date_from=utc_from, date_to=utc_to,
timeframe=cfg.Config().get('calculate.timeframe'),
min_prices=cfg.Config().get('calculate.min_prices'),
max_set_size_diff_pct=cfg.Config().get('calculate.max_set_size_diff_pct'),
overlap_pct=cfg.Config().get('calculate.overlap_pct'),
max_p_value=cfg.Config().get('calculate.max_p_value'))
self.SetStatusText("", 1)
# Show calculated data and refresh frames
self.__on_view_status(evt)
self.__refresh()
def __on_monitor(self, evt):
# Check state of toggle menu. If on, then start monitoring, else stop
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(cfg.Config().get('monitor.interval') * 1000)
# Autosave filename
filename = self.__opened_filename if self.__opened_filename is not None else 'autosave.cpd'
# Build calculation params and start monitor
calculation_params = [cfg.Config().get('monitor.calculations.long'),
cfg.Config().get('monitor.calculations.medium'),
cfg.Config().get('monitor.calculations.short')]
self.cor.start_monitor(interval=cfg.Config().get('monitor.interval'),
calculation_params=calculation_params,
cache_time=cfg.Config().get('monitor.tick_cache_time'),
autosave=cfg.Config().get('monitor.autosave'),
filename=filename)
else:
self.__log.info("Stopping monitoring.")
self.SetStatusText("Not Monitoring", 0)
self.__statusbar.SetBackgroundColour('lightgray')
self.__statusbar.Refresh()
self.timer.Stop()
self.cor.stop_monitor()
def __on_clear(self, evt):
# Clear the history
self.cor.clear_coefficient_history()
# Refresh opened child frames
self.__refresh()
def __on_timer(self, evt):
# Refresh opened child frames
self.__refresh()
# Set status message
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()
def __refresh(self):
"""
Refresh all open child frames
:return:
"""
children = self.GetChildren()
for child in children:
if isinstance(child, CorrelationMDIChild):
child.refresh()
elif isinstance(child, wx.StatusBar):
# Ignore
pass
else:
raise Exception(f"MDI Child for application must implement CorrelationMDIChild.")
class CorrelationMDIChild(wx.MDIChildFrame):
"""
Interface for all MDI Children supported by the MDIParent
"""
@abc.abstractmethod
def refresh(self):
"""
Must be implemented. Refreshes the content. Called by MDIParents __refresh method
:return:
"""
raise NotImplementedError
@@ -0,0 +1,211 @@
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
class MDIChildCorrelationGraph(mdi.CorrelationMDIChild):
"""
Shows the graphs for the specified correlation
"""
symbols = None # Symbols for correlation. Public as we use to check if window for the symbol pair is already open.
# Date formats for graphs
__tick_fmt_date = matplotlib.dates.DateFormatter('%d-%b')
__tick_fmt_time = matplotlib.dates.DateFormatter('%H:%M:%S')
# Colors for graph lines fro symbol1 and symbol2
__colours = ['green', 'blue']
# Fig, axes and canvas
__fig = None
__axs = None
__canvas = None
def __init__(self, parent, symbol1, symbol2):
# Super
wx.MDIChildFrame.__init__(self, parent=parent, id=wx.ID_ANY,
title=f"Correlation Status for {symbol1}:{symbol2}")
# Create logger
self.__log = logging.getLogger(__name__)
# Store the symbols
self.symbols = [symbol1, symbol2]
# We will freeze this frame and thaw once constructed to avoid flicker.
self.Freeze()
# Draw the empty graphs. We will populate with data in refresh. We will have 3 charts:
# 1) Data used to calculate base coefficient for both symbols (2 lines on chart);
# 2) Data used to calculate latest coefficient for both symbols (2 lines on chart); and
# 3) Coefficient history and the divergence threshold lines
# Create fig and 3 axes.
self.__fig, self.__axs = plt.subplots(3)
# Create additional axis for second line on charts 1 & 2
self.__s2axs = [self.__axs[0].twinx(), self.__axs[1].twinx()]
# Set titles
self.__axs[0].set_title(f"Base Coefficient Price Data for {self.symbols[0]}:{self.symbols[1]}")
self.__axs[1].set_title(f"Coefficient Tick Data for {self.symbols[0]}:{self.symbols[1]}")
self.__axs[2].set_title(f"Coefficient History for {self.symbols[0]}:{self.symbols[1]}")
# Set Y Labels and tick colours for charts 1 & 2. Left for symbol1, right for symbol2
for i in range(0, 2):
self.__axs[i].set_ylabel(f"{self.symbols[0]}", color=self.__colours[0])
self.__axs[i].tick_params(axis='y', labelcolor=self.__colours[0])
self.__s2axs[i].set_ylabel(f"{self.symbols[1]}", color=self.__colours[1])
self.__s2axs[i].tick_params(axis='y', labelcolor=self.__colours[1])
# Set Y label and limits for 3rd chart. Limits will be coefficients range from -1 to 1
self.__axs[2].set_ylabel('Coefficient')
self.__axs[2].set_ylim([-1, 1])
# 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 price data for the base coefficient calculation, tick data that was used to calculate last
# coefficient and and the coefficient history data
price_data = [self.GetMDIParent().cor.get_price_data(self.symbols[0]),
self.GetMDIParent().cor.get_price_data(self.symbols[1])]
tick_data = [self.GetMDIParent().cor.get_ticks(self.symbols[0], cache_only=True),
self.GetMDIParent().cor.get_ticks(self.symbols[1], cache_only=True)]
history_data = []
for timeframe in cfg.Config().get('monitor.calculations'):
frm = cfg.Config().get(f'monitor.calculations.{timeframe}.from')
history_data.append(self.GetMDIParent().cor.get_coefficient_history(
{'Symbol 1': self.symbols[0], 'Symbol 2': self.symbols[1], 'Timeframe': frm}))
# Check what data we have available
price_data_available = price_data is not None and len(price_data) == 2 and price_data[0] is not None and \
price_data[1] is not None and len(price_data[0]) > 0 and len(price_data[1]) > 0
tick_data_available = tick_data is not None and len(tick_data) == 2 and tick_data[0] is not None and \
tick_data[1] is not None and len(tick_data[0]) > 0 and len(tick_data[1]) > 0
history_data_available = history_data is not None and len(history_data) > 0
# Get all plots for coefficient history. History can contain multiple plots for different timeframes. They
# will all be plotted on the same chart.
times = []
coefficients = []
if history_data_available:
for hist in history_data:
times.append(hist['Date To'])
coefficients.append(hist['Coefficient'])
# Update graphs where we have data available
if price_data_available:
# Update range and ticks
xrange = [min(min(price_data[0]['time']), min(price_data[1]['time'])),
max(max(price_data[0]['time']), max(price_data[1]['time']))]
self.__axs[0].set_xlim(xrange)
# Plot both lines
self.__axs[0].plot(price_data[0]['time'], price_data[0]['close'],
color=self.__colours[0])
self.__s2axs[0].plot(price_data[1]['time'], price_data[1]['close'],
color=self.__colours[1])
# Ticks, labels and formats. Fixing xticks with FixedLocator but also using MaxNLocator to avoid
# cramped x-labels
if len(price_data[0]['time']) > 0:
self.__axs[0].xaxis.set_major_locator(mticker.MaxNLocator(10))
ticks_loc = self.__axs[0].get_xticks().tolist()
self.__axs[0].xaxis.set_major_locator(mticker.FixedLocator(ticks_loc))
self.__axs[0].set_xticklabels(ticks_loc)
self.__axs[0].xaxis.set_major_formatter(self.__tick_fmt_date)
plt.setp(self.__axs[0].xaxis.get_majorticklabels(), rotation=45)
if tick_data_available:
# Update range and ticks
xrange = [min(min(tick_data[0]['time']), min(tick_data[1]['time'])),
max(max(tick_data[0]['time']), max(tick_data[1]['time']))]
self.__axs[1].set_xlim(xrange)
# Plot both lines
self.__axs[1].plot(tick_data[0]['time'], tick_data[0]['ask'],
color=self.__colours[0])
self.__s2axs[1].plot(tick_data[1]['time'], tick_data[1]['ask'],
color=self.__colours[1])
if len(tick_data[0]['time']) > 0:
self.__axs[1].xaxis.set_major_locator(mticker.MaxNLocator(10))
ticks_loc = self.__axs[1].get_xticks().tolist()
self.__axs[1].xaxis.set_major_locator(mticker.FixedLocator(ticks_loc))
self.__axs[1].set_xticklabels(ticks_loc)
self.__axs[1].xaxis.set_major_formatter(self.__tick_fmt_time)
plt.setp(self.__axs[1].xaxis.get_majorticklabels(), rotation=45)
if history_data_available:
# Plot. There may be more than one set of data for chart. One for each coefficient date range. Convert
# single data to list, then loop to plot
xdata = times if isinstance(times, list) else [times, ]
ydata = coefficients if isinstance(coefficients, list) else [coefficients, ]
for i in range(0, len(xdata)):
self.__axs[2].scatter(xdata[i], ydata[i], s=1)
# Ticks, labels and formats. Fixing xticks with FixedLocator but also using MaxNLocator to avoid
# cramped x-labels
if len(times[0].array) > 0:
self.__axs[2].xaxis.set_major_locator(mticker.MaxNLocator(10))
ticks_loc = self.__axs[2].get_xticks().tolist()
self.__axs[2].xaxis.set_major_locator(mticker.FixedLocator(ticks_loc))
self.__axs[2].set_xticklabels(ticks_loc)
self.__axs[2].xaxis.set_major_formatter(self.__tick_fmt_time)
plt.setp(self.__axs[2].xaxis.get_majorticklabels(), rotation=45)
# Legend
self.__axs[2].legend([f"{cfg.Config().get('monitor.calculations.long.from')} Minutes",
f"{cfg.Config().get('monitor.calculations.medium.from')} Minutes",
f"{cfg.Config().get('monitor.calculations.short.from')} Minutes"])
# Lines showing divergence threshold. 2 if we are monitoring inverse correlations.
divergence_threshold = self.GetMDIParent().cor.divergence_threshold
monitor_inverse = self.GetMDIParent().cor.monitor_inverse
if divergence_threshold is not None:
self.__axs[2].axhline(y=divergence_threshold, color="red", label='_nolegend_', linewidth=1)
if monitor_inverse:
self.__axs[2].axhline(y=divergence_threshold * -1, color="red", label='_nolegend_', linewidth=1)
# Redraw canvas
self.__canvas.draw()
def __del__(self):
# Close all plots
plt.close('all')
+198
View File
@@ -0,0 +1,198 @@
import logging
import pandas as pd
import wx
import wx.grid
from mt5_correlation import correlation as cor
import mt5_correlation.gui.mdi as mdi
# Columns for coefficient table
COLUMN_INDEX = 0
COLUMN_SYMBOL1 = 1
COLUMN_SYMBOL2 = 2
COLUMN_BASE_COEFFICIENT = 3
COLUMN_DATE_FROM = 4
COLUMN_DATE_TO = 5
COLUMN_TIMEFRAME = 6
COLUMN_LAST_CALCULATION = 7
COLUMN_STATUS = 8
class MDIChildStatus(mdi.CorrelationMDIChild):
"""
Shows the status of all correlations that are within the monitoring threshold
"""
# The table and grid containing the status of correlations. Defined at instance level to enable refresh.
__table = None
__grid = None
# Number of rows. Required for and updated by refresh method
__rows = 0
__log = None # The logger
def __init__(self, parent):
# Super
wx.MDIChildFrame.__init__(self, parent=parent, id=wx.ID_ANY, title="Correlation Status",
size=wx.Size(width=440, height=-1), style=wx.DEFAULT_FRAME_STYLE)
# Create logger
self.__log = logging.getLogger(__name__)
# Panel and sizer for table
panel = wx.Panel(self, wx.ID_ANY)
sizer = wx.BoxSizer(wx.VERTICAL)
panel.SetSizer(sizer)
# 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(columns=self.GetMDIParent().cor.filtered_coefficient_data.columns)
self.__grid = wx.grid.Grid(panel, wx.ID_ANY)
self.__grid.SetTable(self.__table, takeOwnership=True)
self.__grid.EnableEditing(False)
self.__grid.EnableDragRowSize(False)
self.__grid.EnableDragColSize(True)
self.__grid.EnableDragGridSize(True)
self.__grid.SetSelectionMode(wx.grid.Grid.SelectRows)
self.__grid.SetRowLabelSize(0)
self.__grid.SetColSize(COLUMN_INDEX, 0) # Index. Hide
self.__grid.SetColSize(COLUMN_SYMBOL1, 100) # Symbol 1
self.__grid.SetColSize(COLUMN_SYMBOL2, 100) # Symbol 2
self.__grid.SetColSize(COLUMN_BASE_COEFFICIENT, 100) # Base Coefficient
self.__grid.SetColSize(COLUMN_DATE_FROM, 0) # UTC Date From. Hide
self.__grid.SetColSize(COLUMN_DATE_TO, 0) # UTC Date To. Hide
self.__grid.SetColSize(COLUMN_TIMEFRAME, 0) # Timeframe. Hide.
self.__grid.SetColSize(COLUMN_LAST_CALCULATION, 0) # Last Calculation. Hide
self.__grid.SetColSize(COLUMN_STATUS, 100) # Status
self.__grid.SetMinSize((420, 500))
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()
def refresh(self):
"""
Refreshes grid. Notifies if rows have been added or deleted.
:return:
"""
self.__log.debug(f"Refreshing grid.")
# Update data
self.__table.data = self.GetMDIParent().cor.filtered_coefficient_data.copy()
# Format
self.__table.data.loc[:, 'Base Coefficient'] = self.__table.data['Base Coefficient'].map('{:.5f}'.format)
self.__table.data.loc[:, 'Last Calculation'] = pd.to_datetime(self.__table.data['Last Calculation'], utc=True)
self.__table.data.loc[:, 'Last Calculation'] = \
self.__table.data['Last Calculation'].dt.strftime('%d-%m-%y %H:%M:%S')
# Start refresh
self.__grid.BeginBatch()
# Check if num rows in dataframe has changed, and send appropriate APPEND or DELETE messages
cur_rows = len(self.GetMDIParent().cor.filtered_coefficient_data.index)
if cur_rows < self.__rows:
# Data has been deleted. Send message
msg = wx.grid.GridTableMessage(self.__table, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED,
self.__rows - cur_rows, self.__rows - cur_rows)
self.__grid.ProcessTableMessage(msg)
elif cur_rows > self.__rows:
# Data has been added. Send message
msg = wx.grid.GridTableMessage(self.__table, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED,
cur_rows - self.__rows) # how many
self.__grid.ProcessTableMessage(msg)
self.__grid.EndBatch()
# Send updated message
msg = wx.grid.GridTableMessage(self.__table, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES)
self.__grid.ProcessTableMessage(msg)
# 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()
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()
class _DataTable(wx.grid.GridTableBase):
"""
A data table that holds data in a pandas dataframe. Contains highlighting rules for status.
"""
data = None # The data for this table. A Pandas DataFrame
def __init__(self, columns):
wx.grid.GridTableBase.__init__(self)
self.headerRows = 1
self.data = pd.DataFrame(columns=columns)
def GetNumberRows(self):
return len(self.data)
def GetNumberCols(self):
return len(self.data.columns) + 1
def GetValue(self, row, col):
if row < self.RowsCount and col < self.ColsCount:
return self.data.index[row] if col == 0 else self.data.iloc[row, col - 1]
else:
raise Exception(f"Trying to access row {row} and col {col} which does not exist.")
def SetValue(self, row, col, value):
self.data.iloc[row, col - 1] = value
def GetColLabelValue(self, col):
if col == 0:
if self.data.index.name is None:
return 'Index'
else:
return self.data.index.name
return str(self.data.columns[col - 1])
def GetTypeName(self, row, col):
return wx.grid.GRID_VALUE_STRING
def GetAttr(self, row, col, prop):
attr = wx.grid.GridCellAttr()
# Check that we are not out of bounds
if row < self.RowsCount:
# If column is status, check and highlight if diverging or converging.
if col in [COLUMN_STATUS]:
# Is status one of interest
value = self.GetValue(row, col)
if value != "":
if value in [cor.STATUS_DIVERGING]:
attr.SetBackgroundColour(wx.RED)
elif value in [cor.STATUS_CONVERGING]:
attr.SetBackgroundColour(wx.GREEN)
else:
attr.SetBackgroundColour(wx.WHITE)
return attr