Added diverged symbols view
This commit is contained in:
@@ -161,6 +161,34 @@ class Correlation:
|
|||||||
|
|
||||||
return filtered_data
|
return filtered_data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def diverged_symbols(self):
|
||||||
|
"""
|
||||||
|
:return: dataframe containing all diverged, diverging or converging symbols and count of number of
|
||||||
|
divergences for those symbols.
|
||||||
|
"""
|
||||||
|
filtered_data = None
|
||||||
|
|
||||||
|
if self.coefficient_data is not None:
|
||||||
|
# Only rows where we have a divergence
|
||||||
|
filtered_data = self.coefficient_data \
|
||||||
|
.loc[(self.coefficient_data['Status'] == STATUS_DIVERGED) |
|
||||||
|
(self.coefficient_data['Status'] == STATUS_DIVERGING) |
|
||||||
|
(self.coefficient_data['Status'] == STATUS_CONVERGING)]
|
||||||
|
|
||||||
|
# We only need the symbols
|
||||||
|
all_symbols = pd.DataFrame(columns=['Symbol', 'Count'],
|
||||||
|
data={'Symbol': filtered_data['Symbol 1'].append(filtered_data['Symbol 2']),
|
||||||
|
'Count': 1})
|
||||||
|
|
||||||
|
# Group and count. Reset index so that we have named SYMBOL column.
|
||||||
|
filtered_data = all_symbols.groupby(by='Symbol').count().reset_index()
|
||||||
|
|
||||||
|
# Sort
|
||||||
|
filtered_data = filtered_data.sort_values('Count', ascending=False)
|
||||||
|
|
||||||
|
return filtered_data
|
||||||
|
|
||||||
def load(self, filename):
|
def load(self, filename):
|
||||||
"""
|
"""
|
||||||
Loads calculated coefficients, price data used to calculate them and tick data used during monitoring.
|
Loads calculated coefficients, price data used to calculate them and tick data used during monitoring.
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ class CorrelationMDIFrame(wx.MDIParentFrame):
|
|||||||
menu_view = wx.Menu()
|
menu_view = wx.Menu()
|
||||||
self.Bind(wx.EVT_MENU, self.__on_view_status, menu_view.Append(wx.ID_ANY, "Status",
|
self.Bind(wx.EVT_MENU, self.__on_view_status, menu_view.Append(wx.ID_ANY, "Status",
|
||||||
"View status of correlations."))
|
"View status of correlations."))
|
||||||
|
self.Bind(wx.EVT_MENU, self.__on_view_diverged, menu_view.Append(wx.ID_ANY, "Diverged Symbols",
|
||||||
|
"View diverged symbols."))
|
||||||
self.menubar.Append(menu_view, "&View")
|
self.menubar.Append(menu_view, "&View")
|
||||||
|
|
||||||
# Set menu bar
|
# Set menu bar
|
||||||
@@ -260,6 +262,20 @@ class CorrelationMDIFrame(wx.MDIParentFrame):
|
|||||||
else:
|
else:
|
||||||
opened_instance.Raise()
|
opened_instance.Raise()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
def __refresh(self):
|
def __refresh(self):
|
||||||
"""
|
"""
|
||||||
Refresh all open child frames
|
Refresh all open child frames
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import logging
|
||||||
|
import pandas as pd
|
||||||
|
import wx
|
||||||
|
import wx.grid
|
||||||
|
|
||||||
|
import mt5_correlation.gui.mdi as mdi
|
||||||
|
|
||||||
|
# Columns for diverged symbols table
|
||||||
|
COLUMN_INDEX = 0
|
||||||
|
COLUMN_SYMBOL = 1
|
||||||
|
COLUMN_NUM_DIVERGENCES = 2
|
||||||
|
|
||||||
|
|
||||||
|
class MDIChildDivergedSymbols(mdi.CorrelationMDIChild):
|
||||||
|
"""
|
||||||
|
Shows the status of all correlations that are within the monitoring threshold
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The table and grid containing the symbols that form part of the diverged 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="Diverged Symbols",
|
||||||
|
size=wx.Size(width=240, 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 grid. This is a data table using pandas dataframe for underlying data. Add the
|
||||||
|
# grid to the sizer.
|
||||||
|
self.__table = _DataTable(columns=self.GetMDIParent().cor.diverged_symbols.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_SYMBOL, 100) # Symbol
|
||||||
|
self.__grid.SetColSize(COLUMN_NUM_DIVERGENCES, 100) # Num divergences
|
||||||
|
self.__grid.SetMinSize((220, 100))
|
||||||
|
sizer.Add(self.__grid, 1, wx.ALL | wx.EXPAND)
|
||||||
|
|
||||||
|
# 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.diverged_symbols.copy()
|
||||||
|
|
||||||
|
# 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.diverged_symbols.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
|
||||||
|
|
||||||
|
|
||||||
|
class _DataTable(wx.grid.GridTableBase):
|
||||||
|
"""
|
||||||
|
A data table that holds data in a pandas dataframe.
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
|
||||||
|
return attr
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch, PropertyMock
|
||||||
import time
|
import time
|
||||||
import mt5_correlation.correlation as correlation
|
import mt5_correlation.correlation as correlation
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -121,7 +121,7 @@ class TestCorrelation(unittest.TestCase):
|
|||||||
self.assertEqual(cor.coefficient_data.iloc[1, 2], 1, "The correlation for SYMBOL1:SYMBOL5 should be 1.")
|
self.assertEqual(cor.coefficient_data.iloc[1, 2], 1, "The correlation for SYMBOL1:SYMBOL5 should be 1.")
|
||||||
self.assertEqual(cor.coefficient_data.iloc[2, 2], 1, "The correlation for SYMBOL4:SYMBOL5 should be 1.")
|
self.assertEqual(cor.coefficient_data.iloc[2, 2], 1, "The correlation for SYMBOL4:SYMBOL5 should be 1.")
|
||||||
|
|
||||||
# Get the price data used to calculate the coefficients fro symbol 1. It should match mock_base_prices.
|
# Get the price data used to calculate the coefficients for symbol 1. It should match mock_base_prices.
|
||||||
price_data = cor.get_price_data('SYMBOL1')
|
price_data = cor.get_price_data('SYMBOL1')
|
||||||
self.assertTrue(price_data.equals(self.mock_base_prices), "Price data returned post calculation should match "
|
self.assertTrue(price_data.equals(self.mock_base_prices), "Price data returned post calculation should match "
|
||||||
"mock price data.")
|
"mock price data.")
|
||||||
@@ -325,6 +325,38 @@ class TestCorrelation(unittest.TestCase):
|
|||||||
# Cleanup. delete the file
|
# Cleanup. delete the file
|
||||||
os.remove("unittest.cpd")
|
os.remove("unittest.cpd")
|
||||||
|
|
||||||
|
@patch('mt5_correlation.correlation.Correlation.coefficient_data', new_callable=PropertyMock)
|
||||||
|
def test_diverged_symbols(self, mock):
|
||||||
|
"""
|
||||||
|
Test that diverged_symbols property correctly groups symbols and counts.
|
||||||
|
:param mock:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# Correlation class
|
||||||
|
cor = correlation.Correlation()
|
||||||
|
|
||||||
|
# Mock the correlation data. Symbol 1 has diverged 3 times; symbols 2 has diverged twice; symbol 3 has
|
||||||
|
# diverged once; symbols 4 has diverged twice and symbol 5 has not diverged at all. Use all diverged status'
|
||||||
|
# (diverged, diverging & converging). Also add a row for a non diverged pair.
|
||||||
|
mock.return_value = pd.DataFrame(columns=['Symbol 1', 'Symbol 2', 'Status'], data=[
|
||||||
|
['SYMBOL1', 'SYMBOL2', correlation.STATUS_DIVERGED],
|
||||||
|
['SYMBOL1', 'SYMBOL3', correlation.STATUS_DIVERGING],
|
||||||
|
['SYMBOL1', 'SYMBOL4', correlation.STATUS_CONVERGING],
|
||||||
|
['SYMBOL2', 'SYMBOL4', correlation.STATUS_DIVERGED],
|
||||||
|
['SYMBOL2', 'SYMBOL3', correlation.STATUS_CORRELATED]])
|
||||||
|
|
||||||
|
# Get the diverged_symbols data and check the counts
|
||||||
|
diverged_symbols = cor.diverged_symbols
|
||||||
|
self.assertEqual(diverged_symbols.loc[(diverged_symbols['Symbol'] == 'SYMBOL1')]['Count'].iloc[0], 3,
|
||||||
|
"Symbol 1 has diverged three times.")
|
||||||
|
self.assertEqual(diverged_symbols.loc[(diverged_symbols['Symbol'] == 'SYMBOL2')]['Count'].iloc[0], 2,
|
||||||
|
"Symbol 2 has diverged twice.")
|
||||||
|
self.assertEqual(diverged_symbols.loc[(diverged_symbols['Symbol'] == 'SYMBOL3')]['Count'].iloc[0], 1,
|
||||||
|
"Symbol 3 has diverged once.")
|
||||||
|
self.assertEqual(diverged_symbols.loc[(diverged_symbols['Symbol'] == 'SYMBOL4')]['Count'].iloc[0], 2,
|
||||||
|
"Symbol 4 has diverged twice.")
|
||||||
|
self.assertFalse('SYMBOL5' in diverged_symbols['Symbol'])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user