UI improvements

This commit is contained in:
Jamie Cash
2021-03-09 16:16:30 +00:00
parent d08fef0cbb
commit 0c17c7e5a9
3 changed files with 136 additions and 77 deletions
+2 -2
View File
@@ -63,8 +63,8 @@ developer:
window:
x: 37
y: 42
width: 1512
height: 975
width: 1510
height: 956
style: 541072960
settings_window:
x: 354
+15 -5
View File
@@ -337,6 +337,18 @@ class Correlation:
return history
def clear_coefficient_history(self):
"""
Clears the coefficient history for all symbol pairs
:return:
"""
# Create dataframes for coefficient history.
coefficient_history_columns = ['Symbol 1', 'Symbol 2', 'Coefficient', 'Timeframe', 'Date From', 'Date To']
self.coefficient_history = pd.DataFrame(columns=coefficient_history_columns)
# Clear tick data
self.__monitor_tick_data = {}
def get_ticks(self, symbol, date_from=None, date_to=None, cache_time=0, cache_only=False):
"""
Returns the ticks for the specified symbol. Get's from cache if available and not older than cache_timeframe.
@@ -539,13 +551,11 @@ class Correlation:
'Timeframe', 'Last Check', 'Last Coefficient']
self.coefficient_data = pd.DataFrame(columns=coefficient_data_columns)
# Create dataframes for coefficient history.
coefficient_history_columns = ['Symbol 1', 'Symbol 2', 'Coefficient', 'Timeframe', 'Date From', 'Date To']
self.coefficient_history = pd.DataFrame(columns=coefficient_history_columns)
# Clear coefficient history
self.clear_coefficient_history()
# Clear price data and tick data
# Clear price data
self.__price_data = None
self.__monitor_tick_data = {}
def __update_coefficient_data(self, symbol1, symbol2, coefficient, timeframe, date_from, date_to):
"""
+119 -70
View File
@@ -4,6 +4,7 @@ import matplotlib.pyplot as plt
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
import matplotlib.dates
import matplotlib
import matplotlib.ticker as mticker
from mt5_correlation.correlation import Correlation
from mt5_correlation.config import Config, SettingsDialog
@@ -53,55 +54,44 @@ class MonitorFrame(wx.Frame):
self.__cor = Correlation()
self.__cor.monitoring_threshold = self.__config.get("monitor.monitoring_threshold")
# Status bar
self.statusbar = self.CreateStatusBar(1)
# Status bar. 2 fields, one for monitoring status and one for general status. On open, monitoring status is not
# monitoring. SetBackgroundColour will change colour of both. Couldn't find a way to set on single field only.
self.__statusbar = self.CreateStatusBar(2)
self.__statusbar.SetStatusWidths([100, -1])
self.SetStatusText("Not Monitoring", 0)
# Menu Bar and file menu
# Menu Bar
self.menubar = wx.MenuBar()
file_menu = wx.Menu()
# File menu items
# File menu and items
file_menu = wx.Menu()
menu_item_open = file_menu.Append(wx.ID_ANY, "Open", "Open correlations file.")
menu_item_save = file_menu.Append(wx.ID_ANY, "Save", "Save correlations file.")
menu_item_saveas = file_menu.Append(wx.ID_ANY, "Save As", "Save correlations file.")
file_menu.AppendSeparator()
menu_item_calculate = file_menu.Append(wx.ID_ANY, "Calculate", "Calculate base coefficients.")
file_menu.AppendSeparator()
menu_item_settings = file_menu.Append(wx.ID_ANY, "Settings", "Change application settings.")
file_menu.AppendSeparator()
menu_item_exit = file_menu.Append(wx.ID_ANY, "Exit", "Close the application")
# Add file menu and set menu bar
self.menubar.Append(file_menu, "File")
# Coefficient menu and items
coef_menu = wx.Menu()
menu_item_calculate = coef_menu.Append(wx.ID_ANY, "Calculate", "Calculate base coefficients.")
self.__menu_item_monitor = coef_menu.Append(wx.ID_ANY, "Monitor", "Monitor correlated pairs for changes to "
"coefficient.", kind=wx.ITEM_CHECK)
coef_menu.AppendSeparator()
menu_item_clear = coef_menu.Append(wx.ID_ANY, "Clear", "Clear coefficient and price history.")
self.menubar.Append(coef_menu, "Coefficient")
# Set menu bar
self.SetMenuBar(self.menubar)
# Main window. We want 2 horizontal sections, the grid showing correlations and a graph. In the correlations
# section, we want 2 vertical sections, the monitor toggle and the correlations grid. For the toggle we want 2
# sections, a label and a toggle.
# ---------------------------------------------------------------
# |label | toggle | |
# |-----------------------| |
# | | |
# |Correlations Grid | Graphs |
# | | |
# | | |
# | | |
# | | |
# ----------------------------------------------------------------
# Main window. We want 2 horizontal sections, the grid showing correlations and a graph.
panel = wx.Panel(self, wx.ID_ANY)
toggle_sizer = wx.BoxSizer(wx.HORIZONTAL) # Label and toggle
correlations_sizer = wx.BoxSizer(wx.VERTICAL) # Toggle sizer and correlations grid
correlations_sizer = wx.BoxSizer(wx.VERTICAL) # Correlations grid
self.__main_sizer = wx.BoxSizer(wx.HORIZONTAL) # Correlations sizer and graphs panel
panel.SetSizer(self.__main_sizer)
# Create the label and toggle, populate the toggle sizer and add the toggle sizer to the correlations sizer
monitor_toggle_label = wx.StaticText(panel, id=wx.ID_ANY, label="Monitoring")
toggle_sizer.Add(monitor_toggle_label, 0, wx.ALL, 1)
self.monitor_toggle = wx.ToggleButton(panel, wx.ID_ANY, label="Off")
self.monitor_toggle.SetBackgroundColour(wx.RED)
toggle_sizer.Add(self.monitor_toggle, 0, wx.ALL, 1)
correlations_sizer.Add(toggle_sizer, 0, wx.ALL, 1)
# 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)
@@ -140,9 +130,6 @@ class MonitorFrame(wx.Frame):
# Set up timer to refresh grid
self.timer = wx.Timer(self)
# Bind monitor button
self.monitor_toggle.Bind(wx.EVT_TOGGLEBUTTON, self.monitor)
# Bind timer
self.Bind(wx.EVT_TIMER, self.__timer_event, self.timer)
@@ -152,6 +139,8 @@ class MonitorFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.save_file_as, menu_item_saveas)
self.Bind(wx.EVT_MENU, self.calculate_coefficients, menu_item_calculate)
self.Bind(wx.EVT_MENU, self.open_settings, menu_item_settings)
self.Bind(wx.EVT_MENU, self.__monitor, self.__menu_item_monitor)
self.Bind(wx.EVT_MENU, self.__clear_history, menu_item_clear)
self.Bind(wx.EVT_MENU, self.quit, menu_item_exit)
# Bind row select
@@ -170,23 +159,23 @@ class MonitorFrame(wx.Frame):
# Load the file chosen by the user.
self.__opened_filename = fileDialog.GetPath()
self.SetStatusText(f"Loading file {self.__opened_filename}.")
self.SetStatusText(f"Loading file {self.__opened_filename}.", 1)
self.__cor.load(self.__opened_filename)
# Refresh data in grid
self.refresh_grid()
self.SetStatusText(f"File {self.__opened_filename} loaded.")
self.SetStatusText(f"File {self.__opened_filename} loaded.", 1)
def save_file(self, event):
self.SetStatusText(f"Saving file as {self.__opened_filename}")
self.SetStatusText(f"Saving file as {self.__opened_filename}", 1)
if self.__opened_filename is None:
self.save_file_as(event)
else:
self.__cor.save(self.__opened_filename)
self.SetStatusText(f"File saved as {self.__opened_filename}")
self.SetStatusText(f"File saved as {self.__opened_filename}", 1)
def save_file_as(self, event):
with wx.FileDialog(self, "Save Coefficients file", wildcard="cpd (*.cpd)|*.cpd",
@@ -195,12 +184,12 @@ class MonitorFrame(wx.Frame):
return # the user changed their mind
# Save the file and price data file, changing opened filename so next save writes to new file
self.SetStatusText(f"Saving file as {self.__opened_filename}")
self.SetStatusText(f"Saving file as {self.__opened_filename}", 1)
self.__opened_filename = fileDialog.GetPath()
self.__cor.save(self.__opened_filename)
self.SetStatusText(f"File saved as {self.__opened_filename}")
self.SetStatusText(f"File saved as {self.__opened_filename}", 1)
def calculate_coefficients(self, event):
# set time zone to UTC to avoid local offset issues, and get from and to dates (a week ago to today)
@@ -209,14 +198,14 @@ class MonitorFrame(wx.Frame):
utc_from = utc_to - timedelta(days=self.__config.get('calculate.from.days'))
# Calculate
self.SetStatusText("Calculating coefficients.")
self.SetStatusText("Calculating coefficients.", 1)
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("")
self.SetStatusText("", 1)
# Show calculated data
self.refresh_grid()
@@ -270,13 +259,13 @@ class MonitorFrame(wx.Frame):
# Update row count
self.__rows = cur_rows
def monitor(self, event):
def __monitor(self, event):
# Check state of toggle button. If on, then start monitoring, else stop
if self.monitor_toggle.GetValue():
self.__log.info("Starting monitoring.")
self.monitor_toggle.SetBackgroundColour(wx.GREEN)
self.monitor_toggle.SetLabelText("On")
self.SetStatusText("Monitoring for changes to coefficients.")
if self.__menu_item_monitor.IsChecked():
self.__log.info("Starting monitoring for changes to coefficients.")
self.SetStatusText("Monitoring", 0)
self.__statusbar.SetBackgroundColour('green')
self.__statusbar.Refresh()
self.timer.Start(self.__config.get('monitor.interval')*1000)
@@ -295,9 +284,9 @@ class MonitorFrame(wx.Frame):
filename=filename)
else:
self.__log.info("Stopping monitoring.")
self.monitor_toggle.SetBackgroundColour(wx.RED)
self.monitor_toggle.SetLabelText("Off")
self.SetStatusText("Monitoring stopped.")
self.SetStatusText("Not Monitoring", 0)
self.__statusbar.SetBackgroundColour('lightgray')
self.__statusbar.Refresh()
self.timer.Stop()
self.__cor.stop_monitor()
@@ -445,6 +434,20 @@ class MonitorFrame(wx.Frame):
if len(self.__selected_correlation) == 2:
self.show_graph(symbol1=self.__selected_correlation[0], symbol2=self.__selected_correlation[1])
def __clear_history(self, event):
"""
Clears the calculated coefficient history and associated price data
:param event:
:return:
"""
# Clear the history
self.__cor.clear_coefficient_history()
# Reload graph if we have a coefficient selected
self.__log.info("History cleared. Reloading graph.")
if len(self.__selected_correlation) == 2:
self.show_graph(symbol1=self.__selected_correlation[0], symbol2=self.__selected_correlation[1])
class DataTable(wx.grid.GridTableBase):
"""
@@ -545,29 +548,39 @@ class GraphPanel(wx.Panel):
:return:
"""
# Check what data we have available
price_data_available = prices is not None and len(prices) == 2 and \
prices[0] is not None and prices[1] is not None
tick_data_available = ticks is not None and len(ticks) == 2 and ticks[0] is not None and ticks[1] is not None
history_data_available = history is not None and len(history) > 0
symbols_selected = symbols is not None and len(symbols) == 2
# Get all plots for history. History can contain multiple plots for different timeframes. They will all be
# plotted on the same chart.
times = []
coefficients = []
for hist in history:
times.append(hist['Date To'])
coefficients.append(hist['Coefficient'])
if history_data_available:
for hist in history:
times.append(hist['Date To'])
coefficients.append(hist['Coefficient'])
# Clear. We will need to redraw
for ax in self.__axes:
ax.clear()
if symbols is not None and len(symbols) == 2:
if symbols_selected:
# Axis ranges
default_range = [datetime.now() - timedelta(days=1), datetime.now()]
price_chart_date_range = default_range # We need a default here if no data is available
if prices is not None and len(prices) == 0 and prices[0] is not None and prices[1] is not None:
if price_data_available:
price_chart_date_range = [min(min(prices[0]['time']), min(prices[1]['time'])),
max(max(prices[0]['time']), max(prices[1]['time']))]
tick_chart_date_range = default_range # We need a default here if no data is available
if ticks is not None and len(ticks) == 0 and ticks[0] is not None and ticks[1] is not None:
else:
price_chart_date_range = [datetime.now() - timedelta(days=1), datetime.now()]
if tick_data_available:
tick_chart_date_range = [min(min(ticks[0]['time']), min(ticks[1]['time'])),
max(max(ticks[0]['time']), max(ticks[1]['time']))]
else:
tick_chart_date_range = [datetime.now() - timedelta(days=1/48), datetime.now()]
# Chart config
titles = [f"Base Coefficient Price Data for {symbols[0]}", f"Base Coefficient Price Data for {symbols[1]}",
@@ -575,15 +588,40 @@ class GraphPanel(wx.Panel):
f"Coefficient History for {symbols[0]}:{symbols[1]}"]
xlims = [price_chart_date_range, price_chart_date_range, tick_chart_date_range, tick_chart_date_range, None]
ylims = [None, None, None, None, [-1, 1]]
# Axis labels
xlabels = [None, None, None, None, None]
ylabels = ['Price', 'Price', 'Price', 'Price', 'Coefficient']
tick_labels = [[], prices[1]['time'], [], ticks[1]['time'], times[0]]
# Tick labels and formats
tick_labels = [[],
prices[1]['time'] if price_data_available else [],
[],
ticks[1]['time'] if tick_data_available else [],
times[0].array if history_data_available else []]
mtick_fmts = [None, self.__tick_fmt_date, None, self.__tick_fmt_time, self.__tick_fmt_time]
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]
# Chart data
xdata = [prices[0]['time'] if price_data_available else [],
prices[1]['time'] if price_data_available else [],
ticks[0]['time'] if tick_data_available else [],
ticks[1]['time'] if tick_data_available else [],
times if history_data_available else []]
ydata = [prices[0]['close'] if price_data_available else [],
prices[1]['close'] if price_data_available else [],
ticks[0]['ask'] if tick_data_available else [],
ticks[1]['ask'] if tick_data_available else [],
coefficients if history_data_available else []]
# Chart types
types = ['plot', 'plot', 'plot', 'plot', 'scatter']
# Legends
legends = [None, None, None, None, [f"{Config().get('monitor.calculate_from.short.minutes')} Minutes",
f"{Config().get('monitor.calculate_from.long.minutes')} Minutes"]]
# Draw 5 charts
for index in range(0, len(self.__axes)):
# Titles and axis labels
@@ -598,12 +636,6 @@ class GraphPanel(wx.Panel):
if ylims[index] is not None:
self.__axes[index].set_ylim(ylims[index])
# Tick labels and formats
self.__axes[index].xaxis.set_ticklabels(tick_labels[index])
if mtick_fmts[index] is not None:
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)
@@ -618,6 +650,23 @@ class GraphPanel(wx.Panel):
elif types[index] == 'scatter':
self.__axes[index].scatter(xdata_list[data_index], ydata_list[data_index], s=1)
# Ticks, labels and formats. Fixing xticks with FixedLocator but also using MaxNLocator to avoid
# cramped x-labels
if len(tick_labels[index]) > 0:
self.__axes[index].xaxis.set_major_locator(mticker.MaxNLocator(10))
ticks_loc = self.__axes[index].get_xticks().tolist()
self.__axes[index].xaxis.set_major_locator(mticker.FixedLocator(ticks_loc))
self.__axes[index].set_xticklabels(ticks_loc)
if mtick_fmts[index] is not None:
self.__axes[index].xaxis.set_major_formatter(mtick_fmts[index])
plt.setp(self.__axes[index].xaxis.get_majorticklabels(), rotation=mtick_rot[index])
else:
self.__axes[index].set_xticklabels([])
# Legends
if legends[index] is not None:
self.__axes[index].legend(legends[index])
# Layout with padding between charts
self.__fig.tight_layout(pad=0.5)