From 81f5fa6c07c5b920e854035579b6fcaf0f0e9b2f Mon Sep 17 00:00:00 2001 From: Jamie Cash Date: Wed, 3 Mar 2021 13:47:19 +0000 Subject: [PATCH] Layout and save window pos --- config.yaml | 8 +- mt5_correlation/config.py | 26 ++++-- mt5_correlation/correlation.py | 18 ++++- mt5_correlation/gui.py | 143 ++++++++++++++++++++------------- 4 files changed, 125 insertions(+), 70 deletions(-) diff --git a/config.yaml b/config.yaml index f894902..fa1d7a2 100644 --- a/config.yaml +++ b/config.yaml @@ -56,5 +56,11 @@ logging: - file propagate: 0 developer: - inspection: true + inspection: false +window: + x: 42 + y: 132 + width: 1459 + height: 894 + style: 541072960 ... \ No newline at end of file diff --git a/mt5_correlation/config.py b/mt5_correlation/config.py index 581736e..e8f3ddc 100644 --- a/mt5_correlation/config.py +++ b/mt5_correlation/config.py @@ -95,6 +95,9 @@ class SettingsDialog(wx.Dialog): box for every value. """ + # Settings + __settings = None # Will set in init. + # Store any settings that have changed changed_settings = {} @@ -170,14 +173,19 @@ class SettingsDialog(wx.Dialog): orig_value = self.__settings.get(setting) new_value = self.changed_settings[setting] + # 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. Note boolean needs to be handled differently as it + # doesn't cast directly. + if isinstance(orig_value, bool): + new_value = new_value.lower() in ['true', '1', 'yes', 't'] + else: + new_value = type(orig_value)(new_value) + # 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 @@ -378,12 +386,12 @@ class SettingsValuePanel(wx.ScrolledWindow): # Store the parent frame and get the settings for this node. self.__parent_frame = parent_frame - settings = Config().get(node) + self.__settings = Config().get(node) leaf_settings = {} - for setting in settings: - if type(settings[setting]) is not dict: - leaf_settings[setting] = settings[setting] + for setting in self.__settings: + if type(self.__settings[setting]) is not dict: + leaf_settings[setting] = self.__settings[setting] # Add the value sizer for settings values. self.__value_sizer = wx.FlexGridSizer(rows=len(leaf_settings), cols=2, vgap=2, hgap=2) @@ -424,7 +432,9 @@ class SettingsValuePanel(wx.ScrolledWindow): """ def on_value_changed(event): + old_val = self.__settings.get(setting_path) self.__parent_frame.changed_settings[setting_path] = event.String - self.__log.debug(f"Value changed for {setting_path}.") + self.__log.debug(f"Value changed from {old_val} to {self.__parent_frame.changed_settings[setting_path]} " + f"for {setting_path}.") return on_value_changed diff --git a/mt5_correlation/correlation.py b/mt5_correlation/correlation.py index b997b8f..8f28396 100644 --- a/mt5_correlation/correlation.py +++ b/mt5_correlation/correlation.py @@ -8,6 +8,8 @@ import threading import pytz from scipy.stats.stats import pearsonr import pickle +import inspect +import sys from mt5_correlation.mt5 import MT5 @@ -28,6 +30,9 @@ class Correlation: __monitoring = False __monitoring_params = {} + # First run of scheduler + __first_run = True + # The price data used to calculate the correlations __price_data = None @@ -391,7 +396,14 @@ class Correlation: 'max_p_value': max_p_value, "cache_time": cache_time, 'autosave': autosave, 'filename': filename} self.__scheduler.enter(delay=interval, priority=1, action=self.__monitor, kwargs=params) - self.__scheduler.run() + + # Log the stack. Debug stack overflow + self.__log.debug(f"Current stack size: {len(inspect.stack())} Recursion limit: {sys.getrecursionlimit()}") + + # Run + if self.__first_run: + self.__first_run = False + self.__scheduler.run() def __update_coefficient(self, symbol1, symbol2, from_mins, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90, max_p_value=0.05, cache_time=10): @@ -460,7 +472,7 @@ class Correlation: return coefficient def __update_all_coefficients(self, from_mins, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90, - max_p_value=0.05, cache_time=10, autosave=False): + max_p_value=0.05, cache_time=10): """ 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. @@ -474,8 +486,6 @@ class Correlation: :param max_p_value: The maximum p value for the correlation to be meaningful :param cache_time: Tick data is cached so that we can check coefficients for multiple symbol pairs and reuse the tick data. Number of seconds to cache tick data for before it becomes stale. - :param autosave: Whether to autosave after every monitor run. If there is no filename set then will create one - named autosave.cpd :return: correlation coefficient, or None if coefficient could not be calculated. """ diff --git a/mt5_correlation/gui.py b/mt5_correlation/gui.py index b84f6d6..d3a6717 100644 --- a/mt5_correlation/gui.py +++ b/mt5_correlation/gui.py @@ -18,10 +18,10 @@ matplotlib.use('WXAgg') class MonitorFrame(wx.Frame): - cor = None - rows = 0 # Need to track as we need to notify grid if row count changes. + __cor = None + __rows = 0 # Need to track as we need to notify grid if row count changes. __opened_filename = None # So we can save to same file as we opened - config = None # The applications config + __config = None # The applications config __selected_correlation = [] # List of Symbol 1 & Symbol 2 @@ -38,15 +38,20 @@ class MonitorFrame(wx.Frame): def __init__(self): # Super - wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title="Divergence Monitor") + wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title="Divergence Monitor", + pos=wx.Point(x=Config().get('window.x'), + y=Config().get('window.y')), + size=wx.Size(width=Config().get('window.width'), + height=Config().get('window.height')), + style=Config().get('window.style')) # Create logger and get config self.__log = logging.getLogger(__name__) - self.config = Config() + self.__config = Config() # Create correlation instance to maintain state of calculated coefficients. Set min coefficient from config - self.cor = Correlation() - self.cor.monitoring_threshold = self.config.get("monitor.monitoring_threshold") + self.__cor = Correlation() + self.__cor.monitoring_threshold = self.__config.get("monitor.monitoring_threshold") # Status bar self.statusbar = self.CreateStatusBar(1) @@ -99,7 +104,7 @@ class MonitorFrame(wx.Frame): # 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(self.cor.filtered_coefficient_data) + self.table = DataTable(self.__cor.filtered_coefficient_data) self.grid_correlations = wx.grid.Grid(panel, wx.ID_ANY) self.grid_correlations.SetTable(self.table, takeOwnership=True) self.grid_correlations.EnableEditing(False) @@ -129,8 +134,7 @@ class MonitorFrame(wx.Frame): self.__main_sizer.Add(correlations_sizer, 0, wx.ALL | wx.EXPAND, 1) self.__main_sizer.Add(self.__graph, 1, wx.ALL | wx.EXPAND, 1) - # Size the window. - self.SetSize((800, 500)) + # Layout the window. self.Layout() # Set up timer to refresh grid @@ -167,7 +171,7 @@ class MonitorFrame(wx.Frame): self.__opened_filename = fileDialog.GetPath() self.SetStatusText(f"Loading file {self.__opened_filename}.") - self.cor.load(self.__opened_filename) + self.__cor.load(self.__opened_filename) # Refresh data in grid self.refresh_grid() @@ -180,7 +184,7 @@ class MonitorFrame(wx.Frame): if self.__opened_filename is None: self.save_file_as(event) else: - self.cor.save(self.__opened_filename) + self.__cor.save(self.__opened_filename) self.SetStatusText(f"File saved as {self.__opened_filename}") @@ -194,7 +198,7 @@ class MonitorFrame(wx.Frame): self.SetStatusText(f"Saving file as {self.__opened_filename}") self.__opened_filename = fileDialog.GetPath() - self.cor.save(self.__opened_filename) + self.__cor.save(self.__opened_filename) self.SetStatusText(f"File saved as {self.__opened_filename}") @@ -202,22 +206,23 @@ class MonitorFrame(wx.Frame): # 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=self.config.get('calculate.from.days')) + utc_from = utc_to - timedelta(days=self.__config.get('calculate.from.days')) # Calculate self.SetStatusText("Calculating coefficients.") - self.cor.calculate(date_from=utc_from, date_to=utc_to, - timeframe=self.config.get('calculate.timeframe'), - min_prices=self.config.get('calculate.min_prices'), - max_set_size_diff_pct=self.config.get('calculate.max_set_size_diff_pct'), - overlap_pct=self.config.get('calculate.overlap_pct'), - max_p_value=self.config.get('calculate.max_p_value')) + self.__cor.calculate(date_from=utc_from, date_to=utc_to, + timeframe=self.__config.get('calculate.timeframe'), + min_prices=self.__config.get('calculate.min_prices'), + max_set_size_diff_pct=self.__config.get('calculate.max_set_size_diff_pct'), + overlap_pct=self.__config.get('calculate.overlap_pct'), + max_p_value=self.__config.get('calculate.max_p_value')) self.SetStatusText("") # Show calculated data self.refresh_grid() def quit(self, event): + # Close self.Close() def refresh_grid(self): @@ -228,7 +233,7 @@ class MonitorFrame(wx.Frame): self.__log.debug(f"Refreshing grid. Timer running: {self.timer.IsRunning()}") # Update data - self.table.data = self.cor.coefficient_data.copy() + self.table.data = self.__cor.coefficient_data.copy() # Format self.table.data.loc[:, 'Base Coefficient'] = self.table.data['Base Coefficient'].map('{:.5f}'.format) @@ -244,16 +249,16 @@ class MonitorFrame(wx.Frame): self.grid_correlations.BeginBatch() # Check if num rows in dataframe has changed, and send appropriate APPEND or DELETE messages - cur_rows = len(self.cor.filtered_coefficient_data.index) - if cur_rows < self.rows: + cur_rows = len(self.__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.__rows - cur_rows, self.__rows - cur_rows) self.grid_correlations.ProcessTableMessage(msg) - elif cur_rows > self.rows: + 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 + cur_rows - self.__rows) # how many self.grid_correlations.ProcessTableMessage(msg) self.grid_correlations.EndBatch() @@ -263,7 +268,7 @@ class MonitorFrame(wx.Frame): self.grid_correlations.ProcessTableMessage(msg) # Update row count - self.rows = cur_rows + self.__rows = cur_rows def monitor(self, event): # Check state of toggle button. If on, then start monitoring, else stop @@ -273,27 +278,27 @@ class MonitorFrame(wx.Frame): self.monitor_toggle.SetLabelText("On") self.SetStatusText("Monitoring for changes to coefficients.") - self.timer.Start(self.config.get('monitor.interval')*1000) + self.timer.Start(self.__config.get('monitor.interval')*1000) # Autosave filename filename = self.__opened_filename if self.__opened_filename is not None else 'autosave.cpd' - self.cor.start_monitor(interval=self.config.get('monitor.interval'), - from_mins=self.config.get('monitor.from.minutes'), - 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'), - cache_time=self.config.get('monitor.tick_cache_time'), - autosave=self.config.get('monitor.autosave'), - filename=filename) + self.__cor.start_monitor(interval=self.__config.get('monitor.interval'), + from_mins=self.__config.get('monitor.from.minutes'), + 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'), + cache_time=self.__config.get('monitor.tick_cache_time'), + autosave=self.__config.get('monitor.autosave'), + filename=filename) else: self.__log.info("Stopping monitoring.") self.monitor_toggle.SetBackgroundColour(wx.RED) self.monitor_toggle.SetLabelText("Off") self.SetStatusText("Monitoring stopped.") self.timer.Stop() - self.cor.stop_monitor() + self.__cor.stop_monitor() def open_settings(self, event): """ @@ -327,25 +332,25 @@ class MonitorFrame(wx.Frame): # Now perform the actions if restart_monitor_timer: self.__log.info("Settings updated. Reloading monitoring timer.") - self.cor.stop_monitor() + 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'), - from_mins=self.config.get('monitor.from.minutes'), - 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')) + utc_from = utc_to - timedelta(minutes=self.__config.get('monitor.from.minutes')) + self.__cor.start_monitor(interval=self.__config.get('monitor.interval'), + from_mins=self.__config.get('monitor.from.minutes'), + 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.info("Settings updated. Restarting gui timer.") self.timer.Stop() - self.timer.Start(self.config.get('monitor.interval') * 1000) + self.timer.Start(self.__config.get('monitor.interval') * 1000) if reload_correlations: self.__log.info("Settings updated. Updating monitoring threshold and reloading grid.") - self.cor.monitoring_threshold = self.config.get("monitor.monitoring_threshold") + self.__cor.monitoring_threshold = self.__config.get("monitor.monitoring_threshold") self.refresh_grid() if reload_logger: @@ -359,10 +364,22 @@ class MonitorFrame(wx.Frame): :param event: :return: """ - if self.__opened_filename is not None: - self.cor.save(self.__opened_filename) + # Save pos and size + x, y = self.GetPosition() + width, height = self.GetSize() + self.__config.set('window.x', x) + self.__config.set('window.y', y) + self.__config.set('window.width', width) + self.__config.set('window.height', height) - self.cor.stop_monitor() + # Style + style = self.GetWindowStyle() + self.__config.set('window.style', style) + + self.__config.save() + + # Stop monitoring + self.__cor.stop_monitor() # Kill graph as it seems to be stopping script from ending self.__graph = None @@ -393,11 +410,11 @@ class MonitorFrame(wx.Frame): """ # Get the price data for the base coefficient calculation, tick data to calculate last coefficient and and the # coefficient history data - symbol_1_price_data = self.cor.get_price_data(symbol1) - symbol_2_price_data = self.cor.get_price_data(symbol2) - symbol_1_ticks = self.cor.get_ticks(symbol1, cache_only=True) - symbol_2_ticks = self.cor.get_ticks(symbol2, cache_only=True) - history_data = self.cor.get_coefficient_history(symbol1, symbol2) + symbol_1_price_data = self.__cor.get_price_data(symbol1) + symbol_2_price_data = self.__cor.get_price_data(symbol2) + symbol_1_ticks = self.__cor.get_ticks(symbol1, cache_only=True) + symbol_2_ticks = self.__cor.get_ticks(symbol2, cache_only=True) + history_data = self.__cor.get_coefficient_history(symbol1, symbol2) times = history_data['UTC Date To'] coefficients = history_data['Coefficient'] @@ -479,6 +496,10 @@ class DataTable(wx.grid.GridTableBase): class GraphPanel(wx.Panel): def __init__(self, parent): + """ + A panel to show the graphs + :param parent: The parent panel + """ # Super wx.Panel.__init__(self, parent) @@ -540,6 +561,7 @@ class GraphPanel(wx.Panel): mtick_rot = [0, 45, 0, 45, 45] xdata = [prices[0]['time'], prices[1]['time'], ticks[0]['time'], ticks[1]['time'], times] ydata = [prices[0]['close'], prices[1]['close'], ticks[0]['ask'], ticks[1]['ask'], coefficients] + types = ['plot', 'plot', 'plot', 'plot', 'scatter'] # Draw 5 charts for index in range(0, len(self.__axes)): @@ -561,8 +583,15 @@ class GraphPanel(wx.Panel): self.__axes[index].xaxis.set_major_formatter(mtick_fmts[index]) plt.setp(self.__axes[index].xaxis.get_majorticklabels(), rotation=mtick_rot[index]) + # Remove typ and right boarders + self.__axes[index].spines["top"].set_visible(False) + self.__axes[index].spines["right"].set_visible(False) + # Plot - self.__axes[index].plot(xdata[index], ydata[index]) + if types[index] == 'plot': + self.__axes[index].plot(xdata[index], ydata[index]) + elif types[index] == 'scatter': + self.__axes[index].scatter(xdata[index], ydata[index], s=1) # Layout with padding between charts self.__fig.tight_layout(pad=0.5)