Added UI and basic monitoring
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
import wx
|
||||
import wx.grid
|
||||
import wx.lib.masked as masked
|
||||
from mt5_correlation.correlation import Correlation
|
||||
from mt5_correlation.config import Config
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
import pandas as pd
|
||||
import logging
|
||||
import definitions
|
||||
|
||||
|
||||
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, *args, **kwds):
|
||||
self.log = logging.getLogger(__name__)
|
||||
self.config = Config.instance()
|
||||
|
||||
# Create correlation instance to maintain state of calculated coefficients
|
||||
self.cor = Correlation()
|
||||
|
||||
kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
|
||||
wx.Frame.__init__(self, *args, **kwds)
|
||||
self.SetSize((1235, 800))
|
||||
self.SetTitle("Monitor for Divergence")
|
||||
|
||||
# Status bar
|
||||
self.statusbar = self.CreateStatusBar(1)
|
||||
|
||||
# Menu Bar
|
||||
self.menubar = wx.MenuBar()
|
||||
file_menu = wx.Menu()
|
||||
|
||||
# Open and save
|
||||
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.")
|
||||
|
||||
# Calculate
|
||||
file_menu.AppendSeparator()
|
||||
menu_item_calculate = file_menu.Append(wx.ID_ANY, "Calculate", "Calculate base coefficients.")
|
||||
|
||||
# Close
|
||||
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
|
||||
self.window = wx.SplitterWindow(self, wx.ID_ANY)
|
||||
self.window.SetMinimumPaneSize(20)
|
||||
|
||||
self.correlations_pane = wx.Panel(self.window, wx.ID_ANY)
|
||||
|
||||
sizer_grid = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Filter label, input box and button
|
||||
sizer_coefficient = wx.BoxSizer(wx.HORIZONTAL)
|
||||
sizer_grid.Add(sizer_coefficient, 0, 0, 0)
|
||||
|
||||
label_min_coefficient = wx.StaticText(self.correlations_pane, wx.ID_ANY, "Min Coefficient (Range -1 - 1)")
|
||||
sizer_coefficient.Add(label_min_coefficient, 0, wx.ALL, 0)
|
||||
|
||||
self.edit_ctrl_min_coefficient = masked.NumCtrl(self.correlations_pane, value=self.cor.min_coefficient,
|
||||
allowNegative=True, min=-1, max=1, integerWidth=1,
|
||||
fractionWidth=5)
|
||||
sizer_coefficient.Add(self.edit_ctrl_min_coefficient, 0, wx.ALL, 0)
|
||||
|
||||
self.filter_button = wx.Button(self.correlations_pane, wx.ID_ANY, label="Filter")
|
||||
sizer_coefficient.Add(self.filter_button, 0, wx.ALL, 0)
|
||||
|
||||
self.monitor_toggle = wx.ToggleButton(self.correlations_pane, wx.ID_ANY, label="Monitoring")
|
||||
sizer_coefficient.Add(self.monitor_toggle, 0, wx.ALL, 0)
|
||||
|
||||
# Data table using pandas dataframe for underlying data
|
||||
self.table = DataTable(self.cor.filtered_coefficient_data)
|
||||
self.grid_correlations = wx.grid.Grid(self.correlations_pane, wx.ID_ANY, size=(1, 1))
|
||||
self.grid_correlations.SetTable(self.table, takeOwnership=True)
|
||||
self.grid_correlations.EnableEditing(0)
|
||||
self.grid_correlations.EnableDragRowSize(0)
|
||||
self.grid_correlations.EnableDragGridSize(0)
|
||||
self.grid_correlations.SetSelectionMode(wx.grid.Grid.SelectRows)
|
||||
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
|
||||
sizer_grid.Add(self.grid_correlations, 1, wx.ALL | wx.EXPAND, 0)
|
||||
|
||||
self.charts_pane = wx.Panel(self.window, wx.ID_ANY)
|
||||
|
||||
# Charts
|
||||
sizer_chart = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
label_2 = wx.StaticText(self.charts_pane, wx.ID_ANY, "Charts Go Here", style=wx.ALIGN_CENTER_HORIZONTAL)
|
||||
sizer_chart.Add(label_2, 0, wx.ALIGN_CENTER_HORIZONTAL, 0)
|
||||
|
||||
# Add the sizers to the panes
|
||||
self.charts_pane.SetSizer(sizer_chart)
|
||||
self.correlations_pane.SetSizer(sizer_grid)
|
||||
|
||||
# Set window split to 2 panes and layout
|
||||
self.window.SplitVertically(self.correlations_pane, self.charts_pane)
|
||||
self.Layout()
|
||||
|
||||
# Set up timer to refresh grid
|
||||
self.timer = wx.Timer(self)
|
||||
|
||||
# Bind my buttons, timer, menu
|
||||
self.filter_button.Bind(wx.EVT_BUTTON, self.change_min_coefficient)
|
||||
self.monitor_toggle.Bind(wx.EVT_TOGGLEBUTTON, self.monitor)
|
||||
self.Bind(wx.EVT_TIMER, self.refresh_grid, self.timer)
|
||||
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.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'))
|
||||
|
||||
# Set timeframe
|
||||
timeframe = self.config.get('calculate.timeframe')
|
||||
|
||||
# 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 change_min_coefficient(self, event):
|
||||
self.cor.min_coefficient = self.edit_ctrl_min_coefficient.GetValue()
|
||||
self.refresh_grid(event)
|
||||
|
||||
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.SetStatusText("Monitoring for changes to coefficients.")
|
||||
|
||||
# Calculate correlations fro last 10 mins
|
||||
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(10000)
|
||||
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.SetStatusText("Monitoring stopped.")
|
||||
self.timer.Stop()
|
||||
self.cor.stop_monitor()
|
||||
|
||||
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
|
||||
# Get application config
|
||||
self.config = Config.instance()
|
||||
|
||||
# Get divergence threshold. This will be used by DataTable to highlight cells
|
||||
self.divergence_threshold = self.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 = self.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
|
||||
Reference in New Issue
Block a user