Split notebook page (tab) out as seperate class from Settings Dialog.

This commit is contained in:
Jamie Cash
2021-02-17 18:24:48 +00:00
parent 2349a362e8
commit 862a55d649
5 changed files with 497 additions and 386 deletions
+4 -4
View File
@@ -9,11 +9,11 @@ calculate:
max_p_value: 0.05 max_p_value: 0.05
monitor: monitor:
from: from:
minutes: 15 minutes: 60
interval: 10 interval: 10
min_prices: 400 min_prices: 1000
max_set_size_diff_pct: 50 max_set_size_diff_pct: 90
overlap_pct: 50 overlap_pct: 90
max_p_value: 0.05 max_p_value: 0.05
monitoring_threshold: 0.9 monitoring_threshold: 0.9
divergence_threshold: 0.8 divergence_threshold: 0.8
+11 -2
View File
@@ -2,11 +2,20 @@
Application to monitor previously correlated symbol pairs for correlation divergence. Application to monitor previously correlated symbol pairs for correlation divergence.
""" """
import definitions import definitions
import yaml
import logging.config import logging.config
from mt5_correlation.gui import MonitorFrame from mt5_correlation.gui import MonitorFrame
from mt5_correlation.config import Config from mt5_correlation.config import Config
import wx import wx
import wx.lib.mixins.inspection as wit
class CorrelationMonitorApp(wx.App, wit.InspectionMixin):
# Override app to use inspection.
# TODO Remove wit.InspectionMixin from overrides when live.
def OnInit(self):
self.Init() # initialize the inspection tool
return True
if __name__ == "__main__": if __name__ == "__main__":
# Load the config # Load the config
@@ -17,7 +26,7 @@ if __name__ == "__main__":
logging.config.dictConfig(log_config) logging.config.dictConfig(log_config)
# Start the app # Start the app
app = wx.App(False) app = CorrelationMonitorApp()
frame = MonitorFrame() frame = MonitorFrame()
frame.Show() frame.Show()
app.MainLoop() app.MainLoop()
+256 -1
View File
@@ -1,5 +1,6 @@
import yaml import yaml
import definitions import wx
import logging
class Config(object): class Config(object):
@@ -86,3 +87,257 @@ class Config(object):
obj = obj[k] obj = obj[k]
obj[key_list[-1]] = value obj[key_list[-1]] = value
class SettingsDialog(wx.Dialog):
# Store any settings that have changed
changed_settings = {}
def __init__(self, *args, **kwargs):
# Super Constructor
wx.Dialog.__init__(self, *args, **kwargs)
self.SetTitle("Settings")
# Create logger and get config
self.__log = logging.getLogger(__name__)
self.__settings = Config()
# Dict of changes. Will commit only on ok
self.__changes = {}
# We want 2 vertical sections, the tabbed notebook and the buttons. The buttons sizer will have 2 horizontal
# sections, one for each button.
main_sizer = wx.BoxSizer(wx.VERTICAL) # Notebook panel
button_sizer = wx.BoxSizer(wx.HORIZONTAL) # Button sizer
# Notebook
self.__notebook = wx.Notebook(self, wx.ID_ANY) # The notebook
# A tab for each root node in config. We will store the tabs components in lists which can be accessed by the
# index returned from notebook.GetSelectedItem()
root_nodes = self.__settings.get_root_nodes()
self.__tabs = []
for node in root_nodes:
# Create new tab
self.__tabs.append(SettingsTab(self, self.__notebook, node))
# Add tab to notebook
self.__notebook.AddPage(self.__tabs[-1], node)
# Buttons
button_ok = wx.Button(self, label="Update")
button_cancel = wx.Button(self, label="Cancel")
button_sizer.Add(button_ok, 0, wx.ALL, 1)
button_sizer.Add(button_cancel, 0, wx.ALL, 1)
# Add notebook and button sizer to main sizer and set main sizer for window
main_sizer.Add(self.__notebook, 1, wx.ALL | wx.EXPAND, 5)
main_sizer.Add(button_sizer)
self.SetSizer(main_sizer)
# Bind buttons & notebook page select.
button_ok.Bind(wx.EVT_BUTTON, self.__on_ok)
button_cancel.Bind(wx.EVT_BUTTON, self.__on_cancel)
self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.__on_page_select)
# Call on_page_select to select the first page
self.__on_page_select(event=None)
def __on_page_select(self, event):
# Call the tabs select method to populate
index = self.__notebook.GetSelection()
self.__tabs[index].select()
def __on_cancel(self, event):
# Clear changed settings and close
self.changed_settings = {}
self.EndModal(wx.ID_CANCEL)
self.Destroy()
def __on_ok(self, event):
# Update settings and save
delkeys = []
for setting in self.changed_settings:
# Get the current and new setting
orig_value = self.__settings.get(setting)
new_value = self.changed_settings[setting]
# If they are the same, discard from changes. We will use a list of items to delete (delkeys) as we cant
# delete whilst iterating. If they are different, update settings.
if orig_value == new_value:
delkeys.append(setting)
else:
# We need to retain data type. New values will all be string as they were retrieved from textctl.
# Get the data type of the original and cast new to it.
new_value = type(orig_value)(new_value)
self.__settings.set(setting, new_value)
# Now delete the items that were the same from changed_settings. changed_settings may be used by settings
# dialog caller.
for key in delkeys:
del(self.changed_settings[key])
# Save the settings and close dialog
self.__settings.save()
self.EndModal(wx.ID_OK)
self.Destroy()
class SettingsTab(wx.Panel):
"""
A notebook tab containing the settings tree and values for a settings root node.
"""
# Parent frame. Set during constructor
__parent_frame = None
# Each tab has: a tree view; a values panel; and a list of value text boxes bound to a change
# event.
__tree = None
__tab_sizer = None
__value_sizer = None
__value_boxes = []
def __init__(self, parent_frame, notebook, root_node):
"""
Creates a tab for the settings notebook.
:param parent_frame: The frame containing the notebook.
:param notebook. The notebook that this tab should be part of.
:param root_node. The root node for the settings
"""
# Super Constructor
wx.Panel.__init__(self, parent=notebook)
# Store the parent frame and get the settings for this tab.
self.__parent_frame = parent_frame
settings = Config().get(root_node)
# Create logger
self.__log = logging.getLogger(__name__)
# Build the tab and set it's sizer.
self.__tab_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.SetSizer(self.__tab_sizer)
# Create tree control
self.__tree = wx.TreeCtrl(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize)
# Add root to tree and use item data to store settings path
root = self.__tree.AddRoot(root_node)
self.__tree.SetItemData(root, root_node)
# Add items to root
self.__tree = self.__build_tree(self.__tree, root, settings)
# expand tree and add it to the sizer
self.__tree.Expand(root)
self.__tab_sizer.Add(self.__tree, 1, wx.ALL | wx.EXPAND, 1)
# Add the value sizer for settings values. This is only used for spacing, as it will be overwritten when
# tree items are selected.
self.__value_sizer = wx.FlexGridSizer(rows=1, cols=2, vgap=2, hgap=2)
self.__tab_sizer.Add(self.__value_sizer, 1, wx.ALL | wx.EXPAND, 1)
label = wx.StaticText(self, wx.ID_ANY, " ".ljust(50), style=wx.ALIGN_LEFT)
self.__value_sizer.Add(label)
# Bind tree selection changed
self.__tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.__on_tree_select)
def select(self):
"""
To be called when this tab is selected. Populate value sizer for the selected item, If no item is selected,
populate for root.
:return:
"""
selected_item = self.__tree.GetSelection()
if selected_item.ID is None:
root_node = self.__tree.GetRootItem()
setting_path = self.__tree.GetItemData(root_node)
else:
setting_path = self.__tree.GetItemData(selected_item)
self.__populate_settings_values(setting_path)
def __build_tree(self, tree, node, settings):
"""
Recursive function to build the tree from the node, using the settings
:param tree: The tree to build
:param node: The tree view node
:param settings: The settings dict for the node.
:return: The built tree
"""
for setting in settings:
# Get value. If dict, add the node and recursively call this function again.
value = settings[setting]
if type(value) is dict:
# Add the node and set its settings path
node_id = tree.AppendItem(node, setting)
current_settings_path = tree.GetItemData(node)
tree.SetItemData(node_id, f"{current_settings_path}.{setting}")
# Recurse
tree = self.__build_tree(tree, node_id, value)
return tree
def __populate_settings_values(self, setting_path):
"""
Populates the settings in the value sizer for a settings path.
:param setting_path:
:return:
"""
# Get the settings for the path
settings = Config().get(setting_path)
# Clear the value sizer and set its rows
self.__value_sizer.Clear(True)
self.__value_sizer.SetRows(len(settings))
# Display every value that is a leaf (not dict)
for setting in settings:
value = settings[setting]
if type(value) is not dict:
# Add a label and value text box
label = wx.StaticText(self, wx.ID_ANY, setting, style=wx.ALIGN_LEFT)
self.__value_boxes.append(wx.TextCtrl(self, wx.ID_ANY, f"{value}", style=wx.ALIGN_LEFT))
self.__value_sizer.AddMany([(label, 0, wx.EXPAND), (self.__value_boxes[-1], 0, wx.EXPAND)])
# Bind to text change. We need to generate a handler as this will have a parameter.
self.__value_boxes[-1].Bind(wx.EVT_TEXT,
self.__get_on_change_evt_handler(setting_path=f'{setting_path}.{setting}'))
self.__value_sizer.Layout()
def __get_on_change_evt_handler(self, setting_path):
"""
Returns a new event handler with a parameter of the settings path
:param setting_path:
:return:
"""
def on_value_changed(event):
self.__parent_frame.changed_settings[setting_path] = event.String
self.__log.debug(f"Value changed for {setting_path}.")
return on_value_changed
def __on_tree_select(self, event):
"""
Called when an item in the tree is selected. Populates the settings values
:param event:
:return:
"""
# Get Selected item and check that it is a tree item
tree_item = event.GetItem()
if not tree_item.IsOk():
return
# Get the setting path from item data
setting_path = self.__tree.GetItemData(tree_item)
# Populate value_sizer
self.__populate_settings_values(setting_path)
+176 -148
View File
@@ -17,22 +17,33 @@ class Correlation:
""" """
# Minimum base coefficient for monitoring. Symbol pairs with a lower correlation # Minimum base coefficient for monitoring. Symbol pairs with a lower correlation
# coefficient than ths wont be monitored. # coefficient than ths won't be monitored.
monitoring_threshold = 0.9 monitoring_threshold = 0.9
# Toggle on whether we are monitoring or not. Set through start_monitor and stop_monitor # Toggle on whether we are monitoring or not. Set through start_monitor and stop_monitor
_monitoring = False __monitoring = False
__monitoring_params = {}
def __init__(self): def __init__(self):
self.log = logging.getLogger(__name__) self.__log = logging.getLogger(__name__)
# Create dataframe # Create dataframe
columns = ['Symbol 1', 'Symbol 2', 'Base Coefficient', 'UTC Date From', 'UTC Date To', 'Timeframe', self.__columns = ['Symbol 1', 'Symbol 2', 'Base Coefficient', 'UTC Date From', 'UTC Date To', 'Timeframe',
'Last Check', 'Last Coefficient'] 'Last Check', 'Last Coefficient']
self.coefficient_data = pd.DataFrame(columns=columns) self.coefficient_data = pd.DataFrame(columns=self.__columns)
# Create timer for continuous monitoring # Create timer for continuous monitoring
self.scheduler = sched.scheduler(time.time, time.sleep) self.__scheduler = sched.scheduler(time.time, time.sleep)
@property
def filtered_coefficient_data(self):
"""
:return: Coefficient data filtered so that all base coefficients >= monitoring_threshold
"""
if self.coefficient_data is not None:
return self.coefficient_data.loc[self.coefficient_data['Base Coefficient'] >= self.monitoring_threshold]
else:
return None
def load(self, filename): def load(self, filename):
""" """
@@ -68,6 +79,14 @@ class Correlation:
:return: :return:
""" """
# If we are monitoring, stop. We will need to restart later
was_monitoring = self.__monitoring
if self.__monitoring:
self.stop_monitor()
# Clear the existing correlations
self.coefficient_data = pd.DataFrame(columns=self.__columns)
# Create mt5 class. This contains required methods for interacting with MT5. # Create mt5 class. This contains required methods for interacting with MT5.
mt5 = MT5() mt5 = MT5()
@@ -112,17 +131,156 @@ class Correlation:
'Base Coefficient': coefficient, 'UTC Date From': date_from, 'Base Coefficient': coefficient, 'UTC Date From': date_from,
'UTC Date To': date_to, 'Timeframe': timeframe}, 'UTC Date To': date_to, 'Timeframe': timeframe},
ignore_index=True) ignore_index=True)
self.log.debug(f"Pair {index} of {num_pair_combinations}: {symbol1}:{symbol2} has a " self.__log.debug(f"Pair {index} of {num_pair_combinations}: {symbol1}:{symbol2} has a "
f"coefficient of {coefficient}.") f"coefficient of {coefficient}.")
else: else:
self.log.debug(f"Coefficient for pair {index} of {num_pair_combinations}: {symbol1}:" self.__log.debug(f"Coefficient for pair {index} of {num_pair_combinations}: {symbol1}:"
f"{symbol2} could no be calculated.") f"{symbol2} could no be calculated.")
# Sort, highest correlated first # Sort, highest correlated first
self.coefficient_data = self.coefficient_data.sort_values('Base Coefficient', ascending=False) self.coefficient_data = self.coefficient_data.sort_values('Base Coefficient', ascending=False)
def update_coefficient(self, symbol1, symbol2, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, # If we were monitoring, we stopped, so start again.
overlap_pct=90, max_p_value=0.05): if was_monitoring:
self.start_monitor(interval=self.__monitoring_params['interval'],
date_from=self.__monitoring_params['date_from'],
date_to=self.__monitoring_params['date_to'],
min_prices=self.__monitoring_params['min_prices'],
max_set_size_diff_pct=self.__monitoring_params['max_set_size_diff_pct'],
overlap_pct=self.__monitoring_params['overlap_pct'],
max_p_value=self.__monitoring_params['max_p_value'])
def start_monitor(self, interval, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90,
max_p_value=0.05):
"""
Starts monitor to continuously update the coefficient for all symbol pairs in that meet the min_coefficient
threshold.
:param interval: How often to check in seconds
:param date_from: From date for tick data from which to calculate correlation coefficients
:param date_to: To date for tick data from which to calculate correlation coefficients
:param min_prices: The minimum number of prices that should be used to calculate coefficient. If this threshold
is not met then returned coefficient will be None
:param max_set_size_diff_pct: Correlations will only be calculated if the sizes of the two price data sets are
within this pct of each other
:param overlap_pct:
:param max_p_value: The maximum p value for the correlation to be meaningful
:return: correlation coefficient, or None if coefficient could not be calculated.
"""
if self.__monitoring:
self.__log.debug(f"Request to start monitor when monitor is already running. Monitor will be stopped and"
f"restarted with new parameters.")
self.stop_monitor()
self.__log.debug(f"Starting monitor.")
self.__monitoring = True
# Create thread to run monitoring This will call private __monitor method that will run the calculation and
# keep scheduling itself while self.monitoring is True. Store the params. We will need to use these if we have
# to stop and restart the monitor. Note, this happens during calculate
self.__monitoring_params = {'interval': interval, 'date_from': date_from, 'date_to': date_to,
'min_prices': min_prices, 'max_set_size_diff_pct': max_set_size_diff_pct,
'overlap_pct': overlap_pct, 'max_p_value': max_p_value}
thread = threading.Thread(target=self.__monitor, kwargs=self.__monitoring_params)
thread.start()
def stop_monitor(self):
"""
Stops monitoring symbol pairs for correlation.
:return:
"""
if self.__monitoring:
self.__log.debug(f"Stopping monitor.")
self.__monitoring = False
else:
self.__log.debug(f"Request to stop monitor when it is not running. No action taken.")
@staticmethod
def calculate_coefficient(symbol1_prices, symbol2_prices, min_prices=100, max_set_size_diff_pct=90,
overlap_pct=90, max_p_value=0.05):
"""
Calculates the correlation coefficient between two sets of price data. Uses close price.
:param symbol1_prices: prices or ticks for symbol 1
:param symbol2_prices: prices or ticks for symbol 2
:param min_prices: The minimum number of prices that should be used to calculate coefficient. If this threshold
is not met then returned coefficient will be None
:param max_set_size_diff_pct: Correlations will only be calculated if the sizes of the two price data sets are
within this pct of each other
:param overlap_pct:
:param max_p_value: The maximum p value for the correlation to be meaningful
:return: correlation coefficient, or None if coefficient could not be calculated.
"""
# Calculate size of intersection and determine if prices for symbols have enough overlapping timestamps for
# correlation coefficient calculation to be meaningful. Is the smallest set at least max_set_size_diff_pct % of
# the size of the largest set and is the overlap set size at least overlap_pct % the size of the smallest set?
coefficient = None
intersect_dates = (set(symbol1_prices['time']) & set(symbol2_prices['time']))
len_smallest_set = int(min([len(symbol1_prices.index), len(symbol2_prices.index)]))
len_largest_set = int(max([len(symbol1_prices.index), len(symbol2_prices.index)]))
similar_size = len_largest_set * (max_set_size_diff_pct / 100) <= len_smallest_set
enough_overlap = len(intersect_dates) >= len_smallest_set * (overlap_pct / 100)
enough_prices = len_smallest_set >= min_prices
suitable = similar_size and enough_overlap and enough_prices
if suitable:
# Calculate coefficient on close prices
# First filter prices to only include those that intersect
symbol1_prices_filtered = symbol1_prices[symbol1_prices['time'].isin(intersect_dates)]
symbol2_prices_filtered = symbol2_prices[symbol2_prices['time'].isin(intersect_dates)]
# Calculate coefficient. Only use if p value is < 0.01 (highly likely that coefficient is valid and null
# hypothesis is false).
coefficient_with_p_value = pearsonr(symbol1_prices_filtered['close'], symbol2_prices_filtered['close'])
coefficient = None if coefficient_with_p_value[1] >= max_p_value else coefficient_with_p_value[0]
# If NaN, change to None
if coefficient is not None and math.isnan(coefficient):
coefficient = None
return coefficient
def __monitor(self, interval, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90,
max_p_value=0.05):
"""
The actual monitor method. Private. This should not be called outside of this class. Use start_monitoring and
stop_monitoring.
:param interval: How often to check in seconds
:param date_from: From date for tick data from which to calculate correlation coefficients
:param date_to: To date for tick data from which to calculate correlation coefficients
:param min_prices: The minimum number of prices that should be used to calculate coefficient. If this threshold
is not met then returned coefficient will be None
:param max_set_size_diff_pct: Correlations will only be calculated if the sizes of the two price data sets are
within this pct of each other
:param overlap_pct:
:param max_p_value: The maximum p value for the correlation to be meaningful
:return: correlation coefficient, or None if coefficient could not be calculated.
"""
self.__log.debug(f"In monitor event. Monitoring: {self.__monitoring}.")
# Only run if monitor is not stopped
if self.__monitoring:
# Update all coefficients
self.__update_all_coefficients(date_from=date_from, date_to=date_to, min_prices=min_prices,
max_set_size_diff_pct=max_set_size_diff_pct, overlap_pct=overlap_pct,
max_p_value=max_p_value)
# Schedule the timer to run again
params = {'interval': interval, 'date_from': date_from, 'date_to': date_to, 'min_prices': min_prices,
'max_set_size_diff_pct': max_set_size_diff_pct, 'overlap_pct': overlap_pct,
'max_p_value': max_p_value}
self.__scheduler.enter(delay=interval, priority=1, action=self.__monitor, kwargs=params)
self.__scheduler.run()
def __update_coefficient(self, symbol1, symbol2, date_from, date_to, min_prices=100, max_set_size_diff_pct=90,
overlap_pct=90, max_p_value=0.05):
""" """
Updates the coefficient for the specified symbol pair Updates the coefficient for the specified symbol pair
:param symbol1: Name of symbol to calculate coefficient for. :param symbol1: Name of symbol to calculate coefficient for.
@@ -181,8 +339,8 @@ class Correlation:
return coefficient return coefficient
def update_all_coefficients(self, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90, def __update_all_coefficients(self, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90,
max_p_value=0.05): max_p_value=0.05):
""" """
Updates the coefficient for all symbol pairs in that meet the min_coefficient threshold. Symbol pairs that meet Updates the coefficient for all symbol pairs in that meet the min_coefficient threshold. Symbol pairs that meet
the threshold can be accessed through the filtered_coefficient_data property. the threshold can be accessed through the filtered_coefficient_data property.
@@ -201,136 +359,6 @@ class Correlation:
for index, row in self.filtered_coefficient_data.iterrows(): for index, row in self.filtered_coefficient_data.iterrows():
symbol1 = row['Symbol 1'] symbol1 = row['Symbol 1']
symbol2 = row['Symbol 2'] symbol2 = row['Symbol 2']
self.update_coefficient(symbol1=symbol1, symbol2=symbol2, date_from=date_from, date_to=date_to, self.__update_coefficient(symbol1=symbol1, symbol2=symbol2, date_from=date_from, date_to=date_to,
min_prices=min_prices, max_set_size_diff_pct=max_set_size_diff_pct, min_prices=min_prices, max_set_size_diff_pct=max_set_size_diff_pct,
overlap_pct=overlap_pct, max_p_value=max_p_value) overlap_pct=overlap_pct, max_p_value=max_p_value)
def start_monitor(self, interval, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90,
max_p_value=0.05):
"""
Starts monitor to continuously update the coefficient for all symbol pairs in that meet the min_coefficient
threshold.
:param interval: How often to check in seconds
:param date_from: From date for tick data from which to calculate correlation coefficients
:param date_to: To date for tick data from which to calculate correlation coefficients
:param min_prices: The minimum number of prices that should be used to calculate coefficient. If this threshold
is not met then returned coefficient will be None
:param max_set_size_diff_pct: Correlations will only be calculated if the sizes of the two price data sets are
within this pct of each other
:param overlap_pct:
:param max_p_value: The maximum p value for the correlation to be meaningful
:return: correlation coefficient, or None if coefficient could not be calculated.
:return:
"""
self.log.debug(f"Starting monitor.")
self._monitoring = True
# Create thread to run monitoring This will call private __monitor method that will run the calculation and
# keep scheduling itself while self.monitoring is True
params = {'interval': interval, 'date_from': date_from, 'date_to': date_to, 'min_prices': min_prices,
'max_set_size_diff_pct': max_set_size_diff_pct, 'overlap_pct': overlap_pct,
'max_p_value': max_p_value}
thread = threading.Thread(target=self.__monitor, kwargs=params)
thread.start()
def stop_monitor(self):
"""
Stops monitoring symbol pairs for correlation.
:return:
"""
self.log.debug(f"Stopping monitor.")
self._monitoring = False
def __monitor(self, interval, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90,
max_p_value=0.05):
"""
The actual monitor method. Private. This should not be called outside of this class. Use start_monitoring and
stop_monitoring.
:param interval: How often to check in seconds
:param date_from: From date for tick data from which to calculate correlation coefficients
:param date_to: To date for tick data from which to calculate correlation coefficients
:param min_prices: The minimum number of prices that should be used to calculate coefficient. If this threshold
is not met then returned coefficient will be None
:param max_set_size_diff_pct: Correlations will only be calculated if the sizes of the two price data sets are
within this pct of each other
:param overlap_pct:
:param max_p_value: The maximum p value for the correlation to be meaningful
:return: correlation coefficient, or None if coefficient could not be calculated.
:return:
"""
self.log.debug(f"In monitor event. Monitoring: {self._monitoring}.")
# Only run if monitor is not stopped
if self._monitoring:
# Update all coefficients
self.update_all_coefficients(date_from=date_from, date_to=date_to, min_prices=min_prices,
max_set_size_diff_pct=max_set_size_diff_pct, overlap_pct=overlap_pct,
max_p_value=max_p_value)
# Schedule the timer to run again
params = {'interval': interval, 'date_from': date_from, 'date_to': date_to, 'min_prices': min_prices,
'max_set_size_diff_pct': max_set_size_diff_pct, 'overlap_pct': overlap_pct,
'max_p_value': max_p_value}
self.scheduler.enter(delay=interval, priority=1, action=self.__monitor, kwargs=params)
self.scheduler.run()
@property
def filtered_coefficient_data(self):
"""
:return: Coefficient data filtered so that all base coefficients >= monitoring_threshold
"""
if self.coefficient_data is not None:
return self.coefficient_data.loc[self.coefficient_data['Base Coefficient'] >= self.monitoring_threshold]
else:
return None
@staticmethod
def calculate_coefficient(symbol1_prices, symbol2_prices, min_prices=100, max_set_size_diff_pct=90,
overlap_pct=90, max_p_value=0.05):
"""
Calculates the correlation coefficient between two sets of price data. Uses close price.
:param symbol1_prices: prices or ticks for symbol 1
:param symbol2_prices: prices or ticks for symbol 2
:param min_prices: The minimum number of prices that should be used to calculate coefficient. If this threshold
is not met then returned coefficient will be None
:param max_set_size_diff_pct: Correlations will only be calculated if the sizes of the two price data sets are
within this pct of each other
:param overlap_pct:
:param max_p_value: The maximum p value for the correlation to be meaningful
:return: correlation coefficient, or None if coefficient could not be calculated.
"""
# Calculate size of intersection and determine if prices for symbols have enough overlapping timestamps for
# correlation coefficient calculation to be meaningful. Is the smallest set at least max_set_size_diff_pct % of
# the size of the largest set and is the overlap set size at least overlap_pct % the size of the smallest set?
coefficient = None
intersect_dates = (set(symbol1_prices['time']) & set(symbol2_prices['time']))
len_smallest_set = int(min([len(symbol1_prices.index), len(symbol2_prices.index)]))
len_largest_set = int(max([len(symbol1_prices.index), len(symbol2_prices.index)]))
similar_size = len_largest_set * (max_set_size_diff_pct / 100) <= len_smallest_set
enough_overlap = len(intersect_dates) >= len_smallest_set * (overlap_pct / 100)
enough_prices = len_smallest_set >= min_prices
suitable = similar_size and enough_overlap and enough_prices
if suitable:
# Calculate coefficient on close prices
# First filter prices to only include those that intersect
symbol1_prices_filtered = symbol1_prices[symbol1_prices['time'].isin(intersect_dates)]
symbol2_prices_filtered = symbol2_prices[symbol2_prices['time'].isin(intersect_dates)]
# Calculate coefficient. Only use if p value is < 0.01 (highly likely that coefficient is valid and null
# hypothesis is false).
coefficient_with_p_value = pearsonr(symbol1_prices_filtered['close'], symbol2_prices_filtered['close'])
coefficient = None if coefficient_with_p_value[1] >= max_p_value else coefficient_with_p_value[0]
# If NaN, change to None
if coefficient is not None and math.isnan(coefficient):
coefficient = None
return coefficient
+50 -231
View File
@@ -1,7 +1,7 @@
import wx import wx
import wx.grid import wx.grid
from mt5_correlation.correlation import Correlation from mt5_correlation.correlation import Correlation
from mt5_correlation.config import Config from mt5_correlation.config import Config, SettingsDialog
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pytz import pytz
import pandas as pd import pandas as pd
@@ -180,9 +180,6 @@ class MonitorFrame(wx.Frame):
utc_to = datetime.now(tz=timezone) utc_to = datetime.now(tz=timezone)
utc_from = utc_to - timedelta(days=self.config.get('calculate.from.days')) utc_from = utc_to - timedelta(days=self.config.get('calculate.from.days'))
# Set timeframe
timeframe = self.config.get('calculate.timeframe')
# Calculate # Calculate
self.SetStatusText("Calculating coefficients.") self.SetStatusText("Calculating coefficients.")
self.cor.calculate(date_from=utc_from, date_to=utc_to, self.cor.calculate(date_from=utc_from, date_to=utc_to,
@@ -252,12 +249,12 @@ class MonitorFrame(wx.Frame):
self.monitor_toggle.SetLabelText("On") self.monitor_toggle.SetLabelText("On")
self.SetStatusText("Monitoring for changes to coefficients.") self.SetStatusText("Monitoring for changes to coefficients.")
# Calculate correlations fro last 10 mins # From and to dates for calculations.
timezone = pytz.timezone("Etc/UTC") timezone = pytz.timezone("Etc/UTC")
utc_to = datetime.now(tz=timezone) utc_to = datetime.now(tz=timezone)
utc_from = utc_to - timedelta(minutes=self.config.get('monitor.from.minutes')) utc_from = utc_to - timedelta(minutes=self.config.get('monitor.from.minutes'))
self.timer.Start(10000) self.timer.Start(self.config.get('monitor.interval')*1000)
self.cor.start_monitor(interval=self.config.get('monitor.interval'), date_from=utc_from, date_to=utc_to, self.cor.start_monitor(interval=self.config.get('monitor.interval'), date_from=utc_from, date_to=utc_to,
min_prices=self.config.get('monitor.min_prices'), min_prices=self.config.get('monitor.min_prices'),
max_set_size_diff_pct=self.config.get('monitor.max_set_size_diff_pct'), max_set_size_diff_pct=self.config.get('monitor.max_set_size_diff_pct'),
@@ -282,10 +279,53 @@ class MonitorFrame(wx.Frame):
# Reload relevant parts of app # Reload relevant parts of app
# TODO: Reload relevant parts of app once settings have changed. Stop and restart monitoring, # TODO: Reload relevant parts of app once settings have changed. Stop and restart monitoring,
# reload logger, filter data, refresh data window. # reload logger, filter data, refresh data window.
self.log.debug("Settings updated. Reloading logger.") restart_monitor_timer = False
log_config = Config().get('logging') restart_gui_timer = False
logging.config.dictConfig(log_config) reload_correlations = False
settings_dialog.Destroy() reload_logger = False
for setting in settings_dialog.changed_settings:
# If any 'monitor.' settings except 'monitor.divergence_threshold have changed then restart
# monitoring timer with new settings.
# If 'monitor.interval has changed then restart gui timer.
# If 'monitor.monitoring_threshold' has changed, then refresh correlation data.
# If any 'logging.' settings have changed, then reload logger config.
if setting.startswith('monitor. ') and setting != 'monitor.divergence_threshold':
restart_monitor_timer = True
if setting == 'monitor.interval':
restart_gui_timer = True
if setting == 'monitor.monitoring_threshold':
reload_correlations = True
if setting.startswith('logging.'):
reload_logger = True
# Now perform the actions
if restart_monitor_timer:
self.log.debug("Settings updated. Reloading monitoring timer.")
self.cor.stop_monitor()
# From and to dates for calculations.
timezone = pytz.timezone("Etc/UTC")
utc_to = datetime.now(tz=timezone)
utc_from = utc_to - timedelta(minutes=self.config.get('monitor.from.minutes'))
self.cor.start_monitor(interval=self.config.get('monitor.interval'), date_from=utc_from, date_to=utc_to,
min_prices=self.config.get('monitor.min_prices'),
max_set_size_diff_pct=self.config.get('monitor.max_set_size_diff_pct'),
overlap_pct=self.config.get('monitor.overlap_pct'),
max_p_value=self.config.get('monitor.max_p_value'))
if restart_gui_timer:
self.log.debug("Settings updated. Restarting gui timer.")
self.timer.Stop()
self.timer.Start(self.config.get('monitor.interval') * 1000)
if reload_correlations:
self.log.debug("Settings updated. Updating monitoring threshold and reloading grid.")
self.cor.monitoring_threshold = self.config.get("monitor.monitoring_threshold")
self.refresh_grid(event)
if reload_logger:
self.log.debug("Settings updated. Reloading logger.")
log_config = Config().get('logging')
logging.config.dictConfig(log_config)
def on_close(self, event): def on_close(self, event):
""" """
@@ -355,224 +395,3 @@ class DataTable(wx.grid.GridTableBase):
attr.SetBackgroundColour(wx.WHITE) attr.SetBackgroundColour(wx.WHITE)
return attr return attr
class SettingsDialog(wx.Dialog):
# Store any settings that have changed
changed_settings = {}
def __init__(self, *args, **kwargs):
# Super Constructor
wx.Dialog.__init__(self, *args, **kwargs)
self.SetTitle("Settings")
# Create logger and get config
self.log = logging.getLogger(__name__)
# Get settings
self.__settings = Config()
# Dict of changes. Will commit only on ok
self.__changes = {}
# We want 2 vertical sections, the tabbed notebook and the buttons. The buttons sizer will have 2 horizontal
# sections, one for each button.
# -------------------------
# |Tabbed notebook |
# | |
# | |
# | |
# | |
# |-----------------------|
# |ok | cancel |
# -------------------------
main_sizer = wx.BoxSizer(wx.VERTICAL) # Notebook panel
button_sizer = wx.BoxSizer(wx.HORIZONTAL) # Button sizer
# Notebook
self.__notebook = wx.Notebook(self, wx.ID_ANY) # The notebook
# A tab for each root node in config. We will store the tabs components in lists which can be accessed by the
# index returned from notebook.GetSelectedItem()
root_nodes = self.__settings.get_root_nodes()
self.__tabs = []
self.__trees = []
self.__tab_sizers = []
self.__value_sizers = []
self.__value_boxes = [] # We need to store these as they will all be bound to a change event
# self.roots = []
for node in root_nodes:
# Create new tab
self.__tabs.append(wx.Panel(self.__notebook, wx.ID_ANY))
self.__tab_sizers.append(wx.BoxSizer(wx.HORIZONTAL))
self.__tabs[-1].SetSizer(self.__tab_sizers[-1])
# Get settings items for tab / node
node_settings = self.__settings.get(node)
# Create tree control
self.__trees.append(wx.TreeCtrl(self.__tabs[-1], wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize))
# Add root to tree and use item data to store settings path
root = self.__trees[-1].AddRoot(node)
self.__trees[-1].SetItemData(root, node)
# Add items to root
self.__trees[-1] = self.__build_tree(self.__trees[-1], root, node_settings)
# expand tree and add it to the sizer
self.__trees[-1].Expand(root)
self.__tab_sizers[-1].Add(self.__trees[-1], 1, wx.ALL | wx.EXPAND, 1)
# Add the value sizer for settings values. This is only used for spacing, as it will be overwritten when
# tree items are selected.
self.__value_sizers.append(wx.FlexGridSizer(rows=1, cols=2, vgap=2, hgap=2))
self.__tab_sizers[-1].Add(self.__value_sizers[-1], 1, wx.ALL | wx.EXPAND, 1)
label = wx.StaticText(self.__tabs[-1], wx.ID_ANY, " ".ljust(50), style=wx.ALIGN_LEFT)
self.__value_sizers[-1].Add(label)
# Add tab to notebook
self.__notebook.AddPage(self.__tabs[-1], node)
# Buttons
button_ok = wx.Button(self, label="Update")
button_cancel = wx.Button(self, label="Cancel")
button_sizer.Add(button_ok, 0, wx.ALL, 1)
button_sizer.Add(button_cancel, 0, wx.ALL, 1)
# Add notebook and button sizer to main sizer and set main sizer for window
main_sizer.Add(self.__notebook, 1, wx.ALL | wx.EXPAND, 5)
main_sizer.Add(button_sizer)
self.SetSizer(main_sizer)
# Bind buttons, notebook page select and tree control select item
button_ok.Bind(wx.EVT_BUTTON, self.__on_ok)
button_cancel.Bind(wx.EVT_BUTTON, self.__on_cancel)
self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.__on_page_select)
self.Bind(wx.EVT_TREE_SEL_CHANGED, self.__on_tree_select)
# Call on_page_select to select the first page
self.__on_page_select(event=None)
def __build_tree(self, tree, node, settings):
"""
Recursive function to build the tree from the node, using the settings
:param tree: The tree to build
:param node: The tree view node
:param settings: The settings dict for the node.
:return: The built tree
"""
for setting in settings:
# Get value. If dict, add the node and recursively call this function again.
value = settings[setting]
if type(value) is dict:
# Add the node and set its settings path
node_id = tree.AppendItem(node, setting)
current_settings_path = tree.GetItemData(node)
tree.SetItemData(node_id, f"{current_settings_path}.{setting}")
# Recurse
tree = self.__build_tree(tree, node_id, value)
return tree
def __populate_settings_values(self, setting_path, index):
"""
Populates the settings in the value sizer for a settings path.
:param setting_path:
:param index: The tab index containing the value_sizer to populate
:return:
"""
# Get the setting values
settings = self.__settings.get(setting_path)
# Get the value sizer, clear it and set its rows
value_sizer = self.__value_sizers[index]
value_sizer.Clear(True)
value_sizer.SetRows(len(settings))
# Display every value that is a leaf (not dict)
for setting in settings:
value = settings[setting]
if type(value) is not dict:
# Add a label and value text box
label = wx.StaticText(self.__tabs[index], wx.ID_ANY, setting, style=wx.ALIGN_LEFT)
self.__value_boxes.append(wx.TextCtrl(self.__tabs[index], wx.ID_ANY, f"{value}", style=wx.ALIGN_LEFT))
value_sizer.AddMany([(label, 0, wx.EXPAND), (self.__value_boxes[-1], 0, wx.EXPAND)])
# Bind to text change. We need to generate a handler as this will have a parameter.
self.__value_boxes[-1].Bind(wx.EVT_TEXT,
self.__get_on_change_evt_handler(setting_path=f'{setting_path}.{setting}'))
value_sizer.Layout()
def __on_page_select(self, event):
# Populate value sizer for the selected item, If no item is selected, populate for root.
index = self.__notebook.GetSelection()
selected_item = self.__trees[index].GetSelection()
if selected_item.ID is None:
root_node = self.__trees[index].GetRootItem()
setting_path = self.__trees[index].GetItemData(root_node)
else:
setting_path = self.__trees[index].GetItemData(selected_item)
self.__populate_settings_values(setting_path, index)
def __on_tree_select(self, event):
# Get Selected item and check that it is a tree item
tree_item = event.GetItem()
if not tree_item.IsOk():
return
# Get the index of the current tab
index = self.__notebook.GetSelection()
# Get the setting path from item data
setting_path = self.__trees[index].GetItemData(tree_item)
# Populate value_sizer
self.__populate_settings_values(setting_path, index)
def __on_cancel(self, e):
# Clear changed settings and close
self.changed_settings = {}
self.EndModal(wx.ID_CANCEL)
self.Destroy()
def __on_ok(self, e):
# Update settings and save
delkeys = []
for setting in self.changed_settings:
# Get the current and new setting
orig_value = self.__settings.get(setting)
new_value = self.changed_settings[setting]
# If they are the same, discard from changes. We will use a list of items to delete (delkeys) as we cant
# delete whilst iterating. If they are different, update settings.
if orig_value == new_value:
delkeys.append(setting)
else:
# We need to retain data type. New values will all be string as they were retrieved from textctl.
# Get the data type of the original and cast new to it.
new_value = type(orig_value)(new_value)
self.__settings.set(setting, new_value)
# Now delete the items that were the same from changed_settings. changed_settings may be used by settings
# dialog caller.
for key in delkeys:
del(self.changed_settings[key])
# Save the settings and close dialog
self.__settings.save()
self.EndModal(wx.ID_OK)
self.Destroy()
def __get_on_change_evt_handler(self, setting_path):
def on_value_changed(event):
self.changed_settings[setting_path] = event.String
self.log.debug(f"Value changed for {setting_path}.")
return on_value_changed