Layout and save window pos

This commit is contained in:
Jamie Cash
2021-03-03 13:47:19 +00:00
parent c85ff8f79e
commit 81f5fa6c07
4 changed files with 125 additions and 70 deletions
+7 -1
View File
@@ -56,5 +56,11 @@ logging:
- file - file
propagate: 0 propagate: 0
developer: developer:
inspection: true inspection: false
window:
x: 42
y: 132
width: 1459
height: 894
style: 541072960
... ...
+18 -8
View File
@@ -95,6 +95,9 @@ class SettingsDialog(wx.Dialog):
box for every value. box for every value.
""" """
# Settings
__settings = None # Will set in init.
# Store any settings that have changed # Store any settings that have changed
changed_settings = {} changed_settings = {}
@@ -170,14 +173,19 @@ class SettingsDialog(wx.Dialog):
orig_value = self.__settings.get(setting) orig_value = self.__settings.get(setting)
new_value = self.changed_settings[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 # 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. # delete whilst iterating. If they are different, update settings.
if orig_value == new_value: if orig_value == new_value:
delkeys.append(setting) delkeys.append(setting)
else: 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) self.__settings.set(setting, new_value)
# Now delete the items that were the same from changed_settings. changed_settings may be used by settings # 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. # Store the parent frame and get the settings for this node.
self.__parent_frame = parent_frame self.__parent_frame = parent_frame
settings = Config().get(node) self.__settings = Config().get(node)
leaf_settings = {} leaf_settings = {}
for setting in settings: for setting in self.__settings:
if type(settings[setting]) is not dict: if type(self.__settings[setting]) is not dict:
leaf_settings[setting] = settings[setting] leaf_settings[setting] = self.__settings[setting]
# Add the value sizer for settings values. # Add the value sizer for settings values.
self.__value_sizer = wx.FlexGridSizer(rows=len(leaf_settings), cols=2, vgap=2, hgap=2) 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): def on_value_changed(event):
old_val = self.__settings.get(setting_path)
self.__parent_frame.changed_settings[setting_path] = event.String 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 return on_value_changed
+14 -4
View File
@@ -8,6 +8,8 @@ import threading
import pytz import pytz
from scipy.stats.stats import pearsonr from scipy.stats.stats import pearsonr
import pickle import pickle
import inspect
import sys
from mt5_correlation.mt5 import MT5 from mt5_correlation.mt5 import MT5
@@ -28,6 +30,9 @@ class Correlation:
__monitoring = False __monitoring = False
__monitoring_params = {} __monitoring_params = {}
# First run of scheduler
__first_run = True
# The price data used to calculate the correlations # The price data used to calculate the correlations
__price_data = None __price_data = None
@@ -391,7 +396,14 @@ class Correlation:
'max_p_value': max_p_value, "cache_time": cache_time, 'autosave': autosave, 'max_p_value': max_p_value, "cache_time": cache_time, 'autosave': autosave,
'filename': filename} 'filename': filename}
self.__scheduler.enter(delay=interval, priority=1, action=self.__monitor, kwargs=params) 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, 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): overlap_pct=90, max_p_value=0.05, cache_time=10):
@@ -460,7 +472,7 @@ class Correlation:
return coefficient return coefficient
def __update_all_coefficients(self, from_mins, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90, 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 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.
@@ -474,8 +486,6 @@ class Correlation:
:param max_p_value: The maximum p value for the correlation to be meaningful :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 :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. 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. :return: correlation coefficient, or None if coefficient could not be calculated.
""" """
+86 -57
View File
@@ -18,10 +18,10 @@ matplotlib.use('WXAgg')
class MonitorFrame(wx.Frame): class MonitorFrame(wx.Frame):
cor = None __cor = None
rows = 0 # Need to track as we need to notify grid if row count changes. __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 __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 __selected_correlation = [] # List of Symbol 1 & Symbol 2
@@ -38,15 +38,20 @@ class MonitorFrame(wx.Frame):
def __init__(self): def __init__(self):
# Super # 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 # Create logger and get config
self.__log = logging.getLogger(__name__) 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 # Create correlation instance to maintain state of calculated coefficients. Set min coefficient from config
self.cor = Correlation() self.__cor = Correlation()
self.cor.monitoring_threshold = self.config.get("monitor.monitoring_threshold") self.__cor.monitoring_threshold = self.__config.get("monitor.monitoring_threshold")
# Status bar # Status bar
self.statusbar = self.CreateStatusBar(1) 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 # Create the correlations grid. This is a data table using pandas dataframe for underlying data. Add the
# correlations_grid to the correlations sizer. # 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 = wx.grid.Grid(panel, wx.ID_ANY)
self.grid_correlations.SetTable(self.table, takeOwnership=True) self.grid_correlations.SetTable(self.table, takeOwnership=True)
self.grid_correlations.EnableEditing(False) 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(correlations_sizer, 0, wx.ALL | wx.EXPAND, 1)
self.__main_sizer.Add(self.__graph, 1, wx.ALL | wx.EXPAND, 1) self.__main_sizer.Add(self.__graph, 1, wx.ALL | wx.EXPAND, 1)
# Size the window. # Layout the window.
self.SetSize((800, 500))
self.Layout() self.Layout()
# Set up timer to refresh grid # Set up timer to refresh grid
@@ -167,7 +171,7 @@ class MonitorFrame(wx.Frame):
self.__opened_filename = fileDialog.GetPath() self.__opened_filename = fileDialog.GetPath()
self.SetStatusText(f"Loading file {self.__opened_filename}.") self.SetStatusText(f"Loading file {self.__opened_filename}.")
self.cor.load(self.__opened_filename) self.__cor.load(self.__opened_filename)
# Refresh data in grid # Refresh data in grid
self.refresh_grid() self.refresh_grid()
@@ -180,7 +184,7 @@ class MonitorFrame(wx.Frame):
if self.__opened_filename is None: if self.__opened_filename is None:
self.save_file_as(event) self.save_file_as(event)
else: else:
self.cor.save(self.__opened_filename) self.__cor.save(self.__opened_filename)
self.SetStatusText(f"File saved as {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.SetStatusText(f"Saving file as {self.__opened_filename}")
self.__opened_filename = fileDialog.GetPath() 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}") 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) # 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") timezone = pytz.timezone("Etc/UTC")
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'))
# 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,
timeframe=self.config.get('calculate.timeframe'), timeframe=self.__config.get('calculate.timeframe'),
min_prices=self.config.get('calculate.min_prices'), min_prices=self.__config.get('calculate.min_prices'),
max_set_size_diff_pct=self.config.get('calculate.max_set_size_diff_pct'), max_set_size_diff_pct=self.__config.get('calculate.max_set_size_diff_pct'),
overlap_pct=self.config.get('calculate.overlap_pct'), overlap_pct=self.__config.get('calculate.overlap_pct'),
max_p_value=self.config.get('calculate.max_p_value')) max_p_value=self.__config.get('calculate.max_p_value'))
self.SetStatusText("") self.SetStatusText("")
# Show calculated data # Show calculated data
self.refresh_grid() self.refresh_grid()
def quit(self, event): def quit(self, event):
# Close
self.Close() self.Close()
def refresh_grid(self): def refresh_grid(self):
@@ -228,7 +233,7 @@ class MonitorFrame(wx.Frame):
self.__log.debug(f"Refreshing grid. Timer running: {self.timer.IsRunning()}") self.__log.debug(f"Refreshing grid. Timer running: {self.timer.IsRunning()}")
# Update data # Update data
self.table.data = self.cor.coefficient_data.copy() self.table.data = self.__cor.coefficient_data.copy()
# Format # Format
self.table.data.loc[:, 'Base Coefficient'] = self.table.data['Base Coefficient'].map('{:.5f}'.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() self.grid_correlations.BeginBatch()
# Check if num rows in dataframe has changed, and send appropriate APPEND or DELETE messages # Check if num rows in dataframe has changed, and send appropriate APPEND or DELETE messages
cur_rows = len(self.cor.filtered_coefficient_data.index) cur_rows = len(self.__cor.filtered_coefficient_data.index)
if cur_rows < self.rows: if cur_rows < self.__rows:
# Data has been deleted. Send message # Data has been deleted. Send message
msg = wx.grid.GridTableMessage(self.table, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, 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) self.grid_correlations.ProcessTableMessage(msg)
elif cur_rows > self.rows: elif cur_rows > self.__rows:
# Data has been added. Send message # Data has been added. Send message
msg = wx.grid.GridTableMessage(self.table, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, 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.ProcessTableMessage(msg)
self.grid_correlations.EndBatch() self.grid_correlations.EndBatch()
@@ -263,7 +268,7 @@ class MonitorFrame(wx.Frame):
self.grid_correlations.ProcessTableMessage(msg) self.grid_correlations.ProcessTableMessage(msg)
# Update row count # Update row count
self.rows = cur_rows self.__rows = cur_rows
def monitor(self, event): def monitor(self, event):
# Check state of toggle button. If on, then start monitoring, else stop # 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.monitor_toggle.SetLabelText("On")
self.SetStatusText("Monitoring for changes to coefficients.") 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 # Autosave filename
filename = self.__opened_filename if self.__opened_filename is not None else 'autosave.cpd' filename = self.__opened_filename if self.__opened_filename is not None else 'autosave.cpd'
self.cor.start_monitor(interval=self.config.get('monitor.interval'), self.__cor.start_monitor(interval=self.__config.get('monitor.interval'),
from_mins=self.config.get('monitor.from.minutes'), from_mins=self.__config.get('monitor.from.minutes'),
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'),
overlap_pct=self.config.get('monitor.overlap_pct'), overlap_pct=self.__config.get('monitor.overlap_pct'),
max_p_value=self.config.get('monitor.max_p_value'), max_p_value=self.__config.get('monitor.max_p_value'),
cache_time=self.config.get('monitor.tick_cache_time'), cache_time=self.__config.get('monitor.tick_cache_time'),
autosave=self.config.get('monitor.autosave'), autosave=self.__config.get('monitor.autosave'),
filename=filename) filename=filename)
else: else:
self.__log.info("Stopping monitoring.") self.__log.info("Stopping monitoring.")
self.monitor_toggle.SetBackgroundColour(wx.RED) self.monitor_toggle.SetBackgroundColour(wx.RED)
self.monitor_toggle.SetLabelText("Off") self.monitor_toggle.SetLabelText("Off")
self.SetStatusText("Monitoring stopped.") self.SetStatusText("Monitoring stopped.")
self.timer.Stop() self.timer.Stop()
self.cor.stop_monitor() self.__cor.stop_monitor()
def open_settings(self, event): def open_settings(self, event):
""" """
@@ -327,25 +332,25 @@ class MonitorFrame(wx.Frame):
# Now perform the actions # Now perform the actions
if restart_monitor_timer: if restart_monitor_timer:
self.__log.info("Settings updated. Reloading monitoring timer.") self.__log.info("Settings updated. Reloading monitoring timer.")
self.cor.stop_monitor() self.__cor.stop_monitor()
# From and to dates for calculations. # 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.cor.start_monitor(interval=self.config.get('monitor.interval'), self.__cor.start_monitor(interval=self.__config.get('monitor.interval'),
from_mins=self.config.get('monitor.from.minutes'), from_mins=self.__config.get('monitor.from.minutes'),
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'),
overlap_pct=self.config.get('monitor.overlap_pct'), overlap_pct=self.__config.get('monitor.overlap_pct'),
max_p_value=self.config.get('monitor.max_p_value')) max_p_value=self.__config.get('monitor.max_p_value'))
if restart_gui_timer: if restart_gui_timer:
self.__log.info("Settings updated. Restarting gui timer.") self.__log.info("Settings updated. Restarting gui timer.")
self.timer.Stop() self.timer.Stop()
self.timer.Start(self.config.get('monitor.interval') * 1000) self.timer.Start(self.__config.get('monitor.interval') * 1000)
if reload_correlations: if reload_correlations:
self.__log.info("Settings updated. Updating monitoring threshold and reloading grid.") 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() self.refresh_grid()
if reload_logger: if reload_logger:
@@ -359,10 +364,22 @@ class MonitorFrame(wx.Frame):
:param event: :param event:
:return: :return:
""" """
if self.__opened_filename is not None: # Save pos and size
self.cor.save(self.__opened_filename) 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 # Kill graph as it seems to be stopping script from ending
self.__graph = None 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 # Get the price data for the base coefficient calculation, tick data to calculate last coefficient and and the
# coefficient history data # coefficient history data
symbol_1_price_data = self.cor.get_price_data(symbol1) symbol_1_price_data = self.__cor.get_price_data(symbol1)
symbol_2_price_data = self.cor.get_price_data(symbol2) symbol_2_price_data = self.__cor.get_price_data(symbol2)
symbol_1_ticks = self.cor.get_ticks(symbol1, cache_only=True) symbol_1_ticks = self.__cor.get_ticks(symbol1, cache_only=True)
symbol_2_ticks = self.cor.get_ticks(symbol2, cache_only=True) symbol_2_ticks = self.__cor.get_ticks(symbol2, cache_only=True)
history_data = self.cor.get_coefficient_history(symbol1, symbol2) history_data = self.__cor.get_coefficient_history(symbol1, symbol2)
times = history_data['UTC Date To'] times = history_data['UTC Date To']
coefficients = history_data['Coefficient'] coefficients = history_data['Coefficient']
@@ -479,6 +496,10 @@ class DataTable(wx.grid.GridTableBase):
class GraphPanel(wx.Panel): class GraphPanel(wx.Panel):
def __init__(self, parent): def __init__(self, parent):
"""
A panel to show the graphs
:param parent: The parent panel
"""
# Super # Super
wx.Panel.__init__(self, parent) wx.Panel.__init__(self, parent)
@@ -540,6 +561,7 @@ class GraphPanel(wx.Panel):
mtick_rot = [0, 45, 0, 45, 45] mtick_rot = [0, 45, 0, 45, 45]
xdata = [prices[0]['time'], prices[1]['time'], ticks[0]['time'], ticks[1]['time'], times] 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] ydata = [prices[0]['close'], prices[1]['close'], ticks[0]['ask'], ticks[1]['ask'], coefficients]
types = ['plot', 'plot', 'plot', 'plot', 'scatter']
# Draw 5 charts # Draw 5 charts
for index in range(0, len(self.__axes)): 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]) self.__axes[index].xaxis.set_major_formatter(mtick_fmts[index])
plt.setp(self.__axes[index].xaxis.get_majorticklabels(), rotation=mtick_rot[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 # 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 # Layout with padding between charts
self.__fig.tight_layout(pad=0.5) self.__fig.tight_layout(pad=0.5)