Files
mt5-correlation/mt5_correlation/gui.py
T

398 lines
18 KiB
Python

import wx
import wx.grid
from mt5_correlation.correlation import Correlation
from mt5_correlation.config import Config, SettingsDialog
from datetime import datetime, timedelta
import pytz
import pandas as pd
import logging
import logging.config
class MonitorFrame(wx.Frame):
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
COLUMN_INDEX = 0
COLUMN_SYMBOL1 = 1
COLUMN_SYMBOL2 = 2
COLUMN_BASE_COEFFICIENT = 3
COLUMN_DATE_FROM = 4
COLUMN_DATE_TO = 5
COLUMN_TIMEFRAME = 6
COLUMN_LAST_CHECK = 7
COLUMN_LAST_COEFFICIENT = 8
def __init__(self):
# Super
wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title="Divergence Monitor")
# Create logger and get config
self.log = logging.getLogger(__name__)
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")
# Status bar
self.statusbar = self.CreateStatusBar(1)
# Menu Bar and file menu
self.menubar = wx.MenuBar()
file_menu = wx.Menu()
# File menu items
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")
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 |
# | | |
# | | |
# | | |
# | | |
# ----------------------------------------------------------------
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
main_sizer = wx.BoxSizer(wx.HORIZONTAL) # Correlations sizer and graphs panel
panel.SetSizer(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)
self.grid_correlations = wx.grid.Grid(panel, wx.ID_ANY)
self.grid_correlations.SetTable(self.table, takeOwnership=True)
self.grid_correlations.EnableEditing(False)
self.grid_correlations.EnableDragRowSize(False)
self.grid_correlations.EnableDragColSize(False)
self.grid_correlations.EnableDragGridSize(False)
self.grid_correlations.SetSelectionMode(wx.grid.Grid.SelectRows)
self.grid_correlations.SetRowLabelSize(0)
self.grid_correlations.SetColSize(self.COLUMN_INDEX, 0) # Index. Hide
self.grid_correlations.SetColSize(self.COLUMN_SYMBOL1, 100) # Symbol 1
self.grid_correlations.SetColSize(self.COLUMN_SYMBOL2, 100) # Symbol 2
self.grid_correlations.SetColSize(self.COLUMN_BASE_COEFFICIENT, 100) # Base Coefficient
self.grid_correlations.SetColSize(self.COLUMN_DATE_FROM, 0) # UTC Date From. Hide
self.grid_correlations.SetColSize(self.COLUMN_DATE_TO, 0) # UTC Date To. Hide
self.grid_correlations.SetColSize(self.COLUMN_TIMEFRAME, 0) # Timeframe. Hide.
self.grid_correlations.SetColSize(self.COLUMN_LAST_CHECK, 100) # Last Check
self.grid_correlations.SetColSize(self.COLUMN_LAST_COEFFICIENT, 100) # Last Coefficient
self.grid_correlations.SetMinSize((520, 500))
self.grid_correlations.SetMaxSize((520, -1))
correlations_sizer.Add(self.grid_correlations, 1, wx.ALL | wx.EXPAND, 1)
# Create the charts
charts = wx.StaticText(panel, wx.ID_ANY, "Charts Go Here", style=wx.ALIGN_CENTER_HORIZONTAL)
# Add the correlations sizer and the charts to the main sizer.
main_sizer.Add(correlations_sizer, 1, wx.ALL | wx.EXPAND, 1)
main_sizer.Add(charts, 1, wx.ALL | wx.EXPAND, 1)
# Size the window.
self.SetSize((800, 500))
self.Layout()
# 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.refresh_grid, self.timer)
# Bind menu items
self.Bind(wx.EVT_MENU, self.open_file, menu_item_open)
self.Bind(wx.EVT_MENU, self.save_file, menu_item_save)
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.quit, menu_item_exit)
# Bind window close event
self.Bind(wx.EVT_CLOSE, self.on_close, self)
def open_file(self, event):
with wx.FileDialog(self, "Open Coefficients file", wildcard="CSV (*.csv)|*.csv",
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return # the user changed their mind
# Load the file chosen by the user
self.opened_filename = fileDialog.GetPath()
self.cor.load(self.opened_filename)
# Refresh data in grid
self.refresh_grid(event)
self.SetStatusText(f"File {self.opened_filename} loaded.")
def save_file(self, event):
self.cor.save(self.opened_filename)
self.SetStatusText(f"File saved as {self.opened_filename}")
def save_file_as(self, event):
with wx.FileDialog(self, "Save Coefficients file", wildcard="CSV (*.csv)|*.csv",
style=wx.FD_SAVE) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return # the user changed their mind
# Save the file, changing opened filename so next save writes to new file
self.opened_filename = fileDialog.GetPath()
self.cor.save(self.opened_filename)
self.SetStatusText(f"File saved as {self.opened_filename}")
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)
timezone = pytz.timezone("Etc/UTC")
utc_to = datetime.now(tz=timezone)
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.SetStatusText("")
# Show calculated data
self.refresh_grid(event)
def quit(self, event):
self.Close()
def refresh_grid(self, event):
"""
Refreshes grid. Notifies if rows have been added or deleted.
:return:
"""
self.log.debug(f"Refreshing grid. Timer running: {self.timer.IsRunning()}")
# Update data
self.table.data = self.cor.coefficient_data.copy()
# Format
self.table.data['Base Coefficient'] = self.table.data['Base Coefficient'].map('{:.5f}'.format)
self.table.data['Last Check'] = pd.to_datetime(self.table.data['Last Check'], utc=True)
self.table.data['Last Check'] = self.table.data['Last Check'].dt.strftime('%d-%m-%y %H:%M:%S')
self.table.data['Last Coefficient'] = self.table.data['Last Coefficient'].map('{:.5f}'.format)
# Remove nans. The ones from the float column wil be str nan as they have been formatted
self.table.data = self.table.data.fillna('')
self.table.data['Last Coefficient'] = self.table.data['Last Coefficient'].replace('nan', '')
# Start refresh
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:
# Data has been deleted. Send message
msg = wx.grid.GridTableMessage(self.table, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED,
self.rows - cur_rows, self.rows - cur_rows)
self.grid_correlations.ProcessTableMessage(msg)
elif cur_rows > self.rows:
# Data has been added. Send message
msg = wx.grid.GridTableMessage(self.table, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED,
cur_rows - self.rows) # how many
self.grid_correlations.ProcessTableMessage(msg)
self.grid_correlations.EndBatch()
# Send updated message
msg = wx.grid.GridTableMessage(self.table, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES)
self.grid_correlations.ProcessTableMessage(msg)
# Update row count
self.rows = cur_rows
def monitor(self, event):
# Check state of toggle button. If on, then start monitoring, else stop
if self.monitor_toggle.GetValue():
self.log.debug("Starting monitoring.")
self.monitor_toggle.SetBackgroundColour(wx.GREEN)
self.monitor_toggle.SetLabelText("On")
self.SetStatusText("Monitoring for changes to coefficients.")
# 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.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,
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'))
else:
self.log.debug("Stopping monitoring.")
self.monitor_toggle.SetBackgroundColour(wx.RED)
self.monitor_toggle.SetLabelText("Off")
self.SetStatusText("Monitoring stopped.")
self.timer.Stop()
self.cor.stop_monitor()
def open_settings(self, event):
"""
Opens the settings dialog
:return:
"""
settings_dialog = SettingsDialog(self)
res = settings_dialog.ShowModal()
if res == wx.ID_OK:
# Reload relevant parts of app
# TODO: Reload relevant parts of app once settings have changed. Stop and restart monitoring,
# reload logger, filter data, refresh data window.
restart_monitor_timer = False
restart_gui_timer = False
reload_correlations = False
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):
"""
Window closing. Save coefficients and stop monitoring.
:param event:
:return:
"""
if self.opened_filename is not None:
self.cor.save(self.opened_filename)
self.cor.stop_monitor()
event.Skip()
class DataTable(wx.grid.GridTableBase):
"""
A data table that holds data in a pandas dataframe
"""
def __init__(self, data=None):
wx.grid.GridTableBase.__init__(self)
self.headerRows = 1
if data is None:
data = pd.DataFrame()
self.data = data
# Get divergence threshold from app config
self.divergence_threshold = Config().get('monitor.divergence_threshold')
def GetNumberRows(self):
return len(self.data)
def GetNumberCols(self):
return len(self.data.columns) + 1
def GetValue(self, row, col):
if col == 0:
return self.data.index[row]
return self.data.iloc[row, col - 1]
def SetValue(self, row, col, value):
self.data.iloc[row, col - 1] = value
def GetColLabelValue(self, col):
if col == 0:
if self.data.index.name is None:
return 'Index'
else:
return self.data.index.name
return str(self.data.columns[col - 1])
def GetTypeName(self, row, col):
return wx.grid.GRID_VALUE_STRING
def GetAttr(self, row, col, prop):
attr = wx.grid.GridCellAttr()
# If column is last coefficient, get value and check against threshold. Highlight if diverged.
threshold = Config().get('monitor.divergence_threshold')
if col == MonitorFrame.COLUMN_LAST_COEFFICIENT:
value = self.GetValue(row, col)
if value != "":
value = float(value)
if value <= threshold:
attr.SetBackgroundColour(wx.YELLOW)
else:
attr.SetBackgroundColour(wx.WHITE)
return attr