From 2349a362e8374489c1e04f7bfbf92d8c602facd4 Mon Sep 17 00:00:00 2001 From: Jamie Cash Date: Tue, 16 Feb 2021 17:15:00 +0000 Subject: [PATCH] Added settings dialog --- config.yaml | 39 +++- logging_conf.yaml | 37 ---- mt5_correlation.py | 14 +- mt5_correlation/config.py | 44 ++-- mt5_correlation/correlation.py | 20 +- mt5_correlation/gui.py | 375 +++++++++++++++++++++++++++------ mt5_correlation/mt5.py | 46 ++-- test/test_config.py | 20 +- 8 files changed, 425 insertions(+), 170 deletions(-) delete mode 100644 logging_conf.yaml diff --git a/config.yaml b/config.yaml index ba10243..084b3ab 100644 --- a/config.yaml +++ b/config.yaml @@ -9,11 +9,48 @@ calculate: max_p_value: 0.05 monitor: from: - minutes: 10 + minutes: 15 interval: 10 min_prices: 400 max_set_size_diff_pct: 50 overlap_pct: 50 max_p_value: 0.05 + monitoring_threshold: 0.9 divergence_threshold: 0.8 +logging: + version: 1 + disable_existing_loggers: false + formatters: + brief: + format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + datefmt: '%H:%M:%S' + precice: + format: '%(asctime)s - %(threadName)s - %(name)s - %(levelname)s - %(message)s' + datefmt: '%Y-%m-%d %H:%M:%S' + handlers: + console: + level: INFO + class: logging.StreamHandler + formatter: brief + stream: ext://sys.stdout + file: + level: DEBUG + class: logging.handlers.RotatingFileHandler + formatter: precice + filename: debug.log + mode: a + maxBytes: 2560000 + backupCount: 1 + root: + level: DEBUG + handlers: + - console + - file + loggers: + mt5-correlation: + level: DEBUG + handlers: + - console + - file + propagate: 0 ... \ No newline at end of file diff --git a/logging_conf.yaml b/logging_conf.yaml deleted file mode 100644 index b314cd7..0000000 --- a/logging_conf.yaml +++ /dev/null @@ -1,37 +0,0 @@ ---- -version: 1 -disable_existing_loggers: False - -formatters: - brief: - format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - datefmt: '%H:%M:%S' - - precice: - format: "%(asctime)s - %(threadName)s - %(name)s - %(levelname)s - %(message)s" - datefmt: '%Y-%m-%d %H:%M:%S' - -handlers: - console: - level: INFO - class: logging.StreamHandler - formatter: brief - stream: ext://sys.stdout - file: - level: DEBUG - class: logging.handlers.RotatingFileHandler - formatter: precice - filename: debug.log - mode: a - maxBytes: 2560000 - backupCount: 1 - -root: - level: DEBUG - handlers: [console, file] - -loggers: - mt5-correlation: - level: DEBUG - handlers: [console, file] - propagate: 0 \ No newline at end of file diff --git a/mt5_correlation.py b/mt5_correlation.py index 2cdff55..2744c37 100644 --- a/mt5_correlation.py +++ b/mt5_correlation.py @@ -9,17 +9,15 @@ from mt5_correlation.config import Config import wx if __name__ == "__main__": - # Configure the logger - with open(fr'{definitions.ROOT_DIR}\logging_conf.yaml', 'rt') as file: - config = yaml.safe_load(file.read()) - logging.config.dictConfig(config) - # Load the config - config = Config.instance() - config.load(fr"{definitions.ROOT_DIR}\config.yaml") + Config().load(fr"{definitions.ROOT_DIR}\config.yaml") + + # Get logging config and configure the logger + log_config = Config().get('logging') + logging.config.dictConfig(log_config) # Start the app app = wx.App(False) - frame = MonitorFrame(None, wx.ID_ANY, "") + frame = MonitorFrame() frame.Show() app.MainLoop() diff --git a/mt5_correlation/config.py b/mt5_correlation/config.py index 320f4d6..7815c0a 100644 --- a/mt5_correlation/config.py +++ b/mt5_correlation/config.py @@ -7,25 +7,18 @@ class Config(object): Provides access to application configuration parameters stored in config.yaml. """ - _config = None - _path = None - _instance = None + config_filepath = None + __config = None + __instance = None - def __init__(self): - """ - Singleton. Raise runtime error - """ - raise RuntimeError('Call instance() instead') - - @classmethod - def instance(cls): + def __new__(cls): """ Singleton. Get instance of this class. Create if not already created. :return: """ - if cls._instance is None: - cls._instance = cls.__new__(cls) - return cls._instance + if cls.__instance is None: + cls.__instance = super(Config, cls).__new__(cls) + return cls.__instance def load(self, path): """ @@ -34,10 +27,10 @@ class Config(object): :return: """ with open(path, 'r') as yamlfile: - self._config = yaml.safe_load(yamlfile) + self.__config = yaml.safe_load(yamlfile) # Store path so that we can save later - self._path = path + self.config_filepath = path def save(self): """ @@ -45,9 +38,9 @@ class Config(object): :return: """ - with open(self._path, 'w') as file: + with open(self.config_filepath, 'w') as file: file.write("---\n") - yaml.dump(self._config, file, sort_keys=False) + yaml.dump(self.__config, file, sort_keys=False) file.write("...") def get(self, path): @@ -62,12 +55,23 @@ class Config(object): for element in elements: if last is None: - last = self._config[element] + last = self.__config[element] else: last = last[element] return last + def get_root_nodes(self): + """ + Returns all root notes as a list + :return: dict of root notes of YAML config file + """ + nodes = [] + for key in self.__config: + nodes.append(key) + + return nodes + def set(self, path, value): """ Sets a config property value @@ -75,7 +79,7 @@ class Config(object): :param value: Value to set property to :return: """ - obj = self._config + obj = self.__config key_list = path.split(".") for k in key_list[:-1]: diff --git a/mt5_correlation/correlation.py b/mt5_correlation/correlation.py index 595e3ea..a63680b 100644 --- a/mt5_correlation/correlation.py +++ b/mt5_correlation/correlation.py @@ -16,8 +16,12 @@ class Correlation: A class to maintain the state of the calculated correlation coefficients. """ - min_coefficient = 0.9 - monitoring = False # Monitoring cor correlations + # Minimum base coefficient for monitoring. Symbol pairs with a lower correlation + # coefficient than ths wont be monitored. + monitoring_threshold = 0.9 + + # Toggle on whether we are monitoring or not. Set through start_monitor and stop_monitor + _monitoring = False def __init__(self): self.log = logging.getLogger(__name__) @@ -221,7 +225,7 @@ class Correlation: :return: """ self.log.debug(f"Starting monitor.") - self.monitoring = True + self._monitoring = True # Create thread to run monitoring This will call private __monitor method that will run the calculation and # keep scheduling itself while self.monitoring is True @@ -237,7 +241,7 @@ class Correlation: :return: """ self.log.debug(f"Stopping monitor.") - self.monitoring = False + self._monitoring = False def __monitor(self, interval, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90, max_p_value=0.05): @@ -257,10 +261,10 @@ class Correlation: :return: correlation coefficient, or None if coefficient could not be calculated. :return: """ - self.log.debug(f"In monitor event. Monitoring: {self.monitoring}.") + self.log.debug(f"In monitor event. Monitoring: {self._monitoring}.") # Only run if monitor is not stopped - if self.monitoring: + if self._monitoring: # Update all coefficients self.update_all_coefficients(date_from=date_from, date_to=date_to, min_prices=min_prices, max_set_size_diff_pct=max_set_size_diff_pct, overlap_pct=overlap_pct, @@ -276,10 +280,10 @@ class Correlation: @property def filtered_coefficient_data(self): """ - :return: Coefficient data filtered so that all base coefficients >= min coefficient + :return: Coefficient data filtered so that all base coefficients >= monitoring_threshold """ if self.coefficient_data is not None: - return self.coefficient_data.loc[self.coefficient_data['Base Coefficient'] >= self.min_coefficient] + return self.coefficient_data.loc[self.coefficient_data['Base Coefficient'] >= self.monitoring_threshold] else: return None diff --git a/mt5_correlation/gui.py b/mt5_correlation/gui.py index 72d6649..a912c47 100644 --- a/mt5_correlation/gui.py +++ b/mt5_correlation/gui.py @@ -1,13 +1,12 @@ 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 +import logging.config class MonitorFrame(wx.Frame): @@ -27,35 +26,33 @@ class MonitorFrame(wx.Frame): COLUMN_LAST_CHECK = 7 COLUMN_LAST_COEFFICIENT = 8 - def __init__(self, *args, **kwds): + 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.instance() + self.config = Config() - # Create correlation instance to maintain state of calculated coefficients + # Create correlation instance to maintain state of calculated coefficients. Set min coefficient from config 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") + self.cor.monitoring_threshold = self.config.get("monitor.monitoring_threshold") # Status bar self.statusbar = self.CreateStatusBar(1) - # Menu Bar + # Menu Bar and file menu self.menubar = wx.MenuBar() file_menu = wx.Menu() - # Open and save + # 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.") - - # Calculate file_menu.AppendSeparator() menu_item_calculate = file_menu.Append(wx.ID_ANY, "Calculate", "Calculate base coefficients.") - - # Close + 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") @@ -63,40 +60,44 @@ class MonitorFrame(wx.Frame): self.menubar.Append(file_menu, "File") self.SetMenuBar(self.menubar) - # Main window - self.window = wx.SplitterWindow(self, wx.ID_ANY) - self.window.SetMinimumPaneSize(20) + # 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) - self.correlations_pane = wx.Panel(self.window, wx.ID_ANY) + # 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) - 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 + # 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(self.correlations_pane, wx.ID_ANY, size=(1, 1)) + self.grid_correlations = wx.grid.Grid(panel, wx.ID_ANY) 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.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 @@ -106,35 +107,36 @@ class MonitorFrame(wx.Frame): 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.grid_correlations.SetMinSize((520, 500)) + self.grid_correlations.SetMaxSize((520, -1)) + correlations_sizer.Add(self.grid_correlations, 1, wx.ALL | wx.EXPAND, 1) - self.charts_pane = wx.Panel(self.window, wx.ID_ANY) + # Create the charts + charts = wx.StaticText(panel, wx.ID_ANY, "Charts Go Here", style=wx.ALIGN_CENTER_HORIZONTAL) - # Charts - sizer_chart = wx.BoxSizer(wx.VERTICAL) + # 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) - 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) + # Size the window. + self.SetSize((800, 500)) 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) + # 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 @@ -197,10 +199,6 @@ class MonitorFrame(wx.Frame): 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. @@ -250,6 +248,8 @@ class MonitorFrame(wx.Frame): # 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.") # Calculate correlations fro last 10 mins @@ -265,10 +265,28 @@ class MonitorFrame(wx.Frame): 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. + self.log.debug("Settings updated. Reloading logger.") + log_config = Config().get('logging') + logging.config.dictConfig(log_config) + settings_dialog.Destroy() + def on_close(self, event): """ Window closing. Save coefficients and stop monitoring. @@ -295,11 +313,7 @@ class DataTable(wx.grid.GridTableBase): 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') + self.divergence_threshold = Config().get('monitor.divergence_threshold') def GetNumberRows(self): return len(self.data) @@ -330,7 +344,7 @@ class DataTable(wx.grid.GridTableBase): 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') + threshold = Config().get('monitor.divergence_threshold') if col == MonitorFrame.COLUMN_LAST_COEFFICIENT: value = self.GetValue(row, col) if value != "": @@ -341,3 +355,224 @@ class DataTable(wx.grid.GridTableBase): attr.SetBackgroundColour(wx.WHITE) return attr + + +class SettingsDialog(wx.Dialog): + + # Store any settings that have changed + changed_settings = {} + + def __init__(self, *args, **kwargs): + # Super Constructor + wx.Dialog.__init__(self, *args, **kwargs) + self.SetTitle("Settings") + + # Create logger and get config + self.log = logging.getLogger(__name__) + + # Get settings + self.__settings = Config() + + # Dict of changes. Will commit only on ok + self.__changes = {} + + # We want 2 vertical sections, the tabbed notebook and the buttons. The buttons sizer will have 2 horizontal + # sections, one for each button. + # ------------------------- + # |Tabbed notebook | + # | | + # | | + # | | + # | | + # |-----------------------| + # |ok | cancel | + # ------------------------- + main_sizer = wx.BoxSizer(wx.VERTICAL) # Notebook panel + button_sizer = wx.BoxSizer(wx.HORIZONTAL) # Button sizer + + # Notebook + self.__notebook = wx.Notebook(self, wx.ID_ANY) # The notebook + + # A tab for each root node in config. We will store the tabs components in lists which can be accessed by the + # index returned from notebook.GetSelectedItem() + root_nodes = self.__settings.get_root_nodes() + self.__tabs = [] + self.__trees = [] + self.__tab_sizers = [] + self.__value_sizers = [] + self.__value_boxes = [] # We need to store these as they will all be bound to a change event + # self.roots = [] + for node in root_nodes: + # Create new tab + self.__tabs.append(wx.Panel(self.__notebook, wx.ID_ANY)) + self.__tab_sizers.append(wx.BoxSizer(wx.HORIZONTAL)) + self.__tabs[-1].SetSizer(self.__tab_sizers[-1]) + + # Get settings items for tab / node + node_settings = self.__settings.get(node) + + # Create tree control + self.__trees.append(wx.TreeCtrl(self.__tabs[-1], wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize)) + + # Add root to tree and use item data to store settings path + root = self.__trees[-1].AddRoot(node) + self.__trees[-1].SetItemData(root, node) + + # Add items to root + self.__trees[-1] = self.__build_tree(self.__trees[-1], root, node_settings) + + # expand tree and add it to the sizer + self.__trees[-1].Expand(root) + self.__tab_sizers[-1].Add(self.__trees[-1], 1, wx.ALL | wx.EXPAND, 1) + + # Add the value sizer for settings values. This is only used for spacing, as it will be overwritten when + # tree items are selected. + self.__value_sizers.append(wx.FlexGridSizer(rows=1, cols=2, vgap=2, hgap=2)) + self.__tab_sizers[-1].Add(self.__value_sizers[-1], 1, wx.ALL | wx.EXPAND, 1) + label = wx.StaticText(self.__tabs[-1], wx.ID_ANY, " ".ljust(50), style=wx.ALIGN_LEFT) + self.__value_sizers[-1].Add(label) + + # Add tab to notebook + self.__notebook.AddPage(self.__tabs[-1], node) + + # Buttons + button_ok = wx.Button(self, label="Update") + button_cancel = wx.Button(self, label="Cancel") + button_sizer.Add(button_ok, 0, wx.ALL, 1) + button_sizer.Add(button_cancel, 0, wx.ALL, 1) + + # Add notebook and button sizer to main sizer and set main sizer for window + main_sizer.Add(self.__notebook, 1, wx.ALL | wx.EXPAND, 5) + main_sizer.Add(button_sizer) + self.SetSizer(main_sizer) + + # Bind buttons, notebook page select and tree control select item + button_ok.Bind(wx.EVT_BUTTON, self.__on_ok) + button_cancel.Bind(wx.EVT_BUTTON, self.__on_cancel) + self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.__on_page_select) + self.Bind(wx.EVT_TREE_SEL_CHANGED, self.__on_tree_select) + + # Call on_page_select to select the first page + self.__on_page_select(event=None) + + def __build_tree(self, tree, node, settings): + """ + Recursive function to build the tree from the node, using the settings + :param tree: The tree to build + :param node: The tree view node + :param settings: The settings dict for the node. + :return: The built tree + """ + for setting in settings: + # Get value. If dict, add the node and recursively call this function again. + value = settings[setting] + if type(value) is dict: + # Add the node and set its settings path + node_id = tree.AppendItem(node, setting) + current_settings_path = tree.GetItemData(node) + tree.SetItemData(node_id, f"{current_settings_path}.{setting}") + + # Recurse + tree = self.__build_tree(tree, node_id, value) + + return tree + + def __populate_settings_values(self, setting_path, index): + """ + Populates the settings in the value sizer for a settings path. + + :param setting_path: + :param index: The tab index containing the value_sizer to populate + + :return: + """ + # Get the setting values + settings = self.__settings.get(setting_path) + + # Get the value sizer, clear it and set its rows + value_sizer = self.__value_sizers[index] + value_sizer.Clear(True) + value_sizer.SetRows(len(settings)) + + # Display every value that is a leaf (not dict) + for setting in settings: + value = settings[setting] + if type(value) is not dict: + # Add a label and value text box + label = wx.StaticText(self.__tabs[index], wx.ID_ANY, setting, style=wx.ALIGN_LEFT) + self.__value_boxes.append(wx.TextCtrl(self.__tabs[index], wx.ID_ANY, f"{value}", style=wx.ALIGN_LEFT)) + value_sizer.AddMany([(label, 0, wx.EXPAND), (self.__value_boxes[-1], 0, wx.EXPAND)]) + + # Bind to text change. We need to generate a handler as this will have a parameter. + self.__value_boxes[-1].Bind(wx.EVT_TEXT, + self.__get_on_change_evt_handler(setting_path=f'{setting_path}.{setting}')) + + value_sizer.Layout() + + def __on_page_select(self, event): + # Populate value sizer for the selected item, If no item is selected, populate for root. + index = self.__notebook.GetSelection() + selected_item = self.__trees[index].GetSelection() + if selected_item.ID is None: + root_node = self.__trees[index].GetRootItem() + setting_path = self.__trees[index].GetItemData(root_node) + else: + setting_path = self.__trees[index].GetItemData(selected_item) + + self.__populate_settings_values(setting_path, index) + + def __on_tree_select(self, event): + # Get Selected item and check that it is a tree item + tree_item = event.GetItem() + if not tree_item.IsOk(): + return + + # Get the index of the current tab + index = self.__notebook.GetSelection() + + # Get the setting path from item data + setting_path = self.__trees[index].GetItemData(tree_item) + + # Populate value_sizer + self.__populate_settings_values(setting_path, index) + + def __on_cancel(self, e): + # Clear changed settings and close + self.changed_settings = {} + self.EndModal(wx.ID_CANCEL) + self.Destroy() + + def __on_ok(self, e): + # Update settings and save + delkeys = [] + for setting in self.changed_settings: + # Get the current and new setting + orig_value = self.__settings.get(setting) + new_value = self.changed_settings[setting] + + # 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 + # dialog caller. + for key in delkeys: + del(self.changed_settings[key]) + + # Save the settings and close dialog + self.__settings.save() + self.EndModal(wx.ID_OK) + self.Destroy() + + def __get_on_change_evt_handler(self, setting_path): + def on_value_changed(event): + self.changed_settings[setting_path] = event.String + self.log.debug(f"Value changed for {setting_path}.") + + return on_value_changed diff --git a/mt5_correlation/mt5.py b/mt5_correlation/mt5.py index 184126f..11bf8a3 100644 --- a/mt5_correlation/mt5.py +++ b/mt5_correlation/mt5.py @@ -2,35 +2,35 @@ import pandas as pd import MetaTrader5 import logging +# Timeframes +TIMEFRAME_M1 = MetaTrader5.TIMEFRAME_M1 +TIMEFRAME_M2 = MetaTrader5.TIMEFRAME_M2 +TIMEFRAME_M3 = MetaTrader5.TIMEFRAME_M3 +TIMEFRAME_M4 = MetaTrader5.TIMEFRAME_M4 +TIMEFRAME_M5 = MetaTrader5.TIMEFRAME_M5 +TIMEFRAME_M6 = MetaTrader5.TIMEFRAME_M6 +TIMEFRAME_M10 = MetaTrader5.TIMEFRAME_M10 +TIMEFRAME_M12 = MetaTrader5.TIMEFRAME_M10 +TIMEFRAME_M15 = MetaTrader5.TIMEFRAME_M15 +TIMEFRAME_M20 = MetaTrader5.TIMEFRAME_M20 +TIMEFRAME_M30 = MetaTrader5.TIMEFRAME_M30 +TIMEFRAME_H1 = MetaTrader5.TIMEFRAME_H1 +TIMEFRAME_H2 = MetaTrader5.TIMEFRAME_H2 +TIMEFRAME_H3 = MetaTrader5.TIMEFRAME_H3 +TIMEFRAME_H4 = MetaTrader5.TIMEFRAME_H4 +TIMEFRAME_H6 = MetaTrader5.TIMEFRAME_H6 +TIMEFRAME_H8 = MetaTrader5.TIMEFRAME_H8 +TIMEFRAME_H12 = MetaTrader5.TIMEFRAME_H12 +TIMEFRAME_D1 = MetaTrader5.TIMEFRAME_D1 +TIMEFRAME_W1 = MetaTrader5.TIMEFRAME_W1 +TIMEFRAME_MN1 = MetaTrader5.TIMEFRAME_MN1 + class MT5: """ A class to connect to and interface with MetaTrader 5 """ - # Timeframes - TIMEFRAME_M1 = MetaTrader5.TIMEFRAME_M1 - TIMEFRAME_M2 = MetaTrader5.TIMEFRAME_M2 - TIMEFRAME_M3 = MetaTrader5.TIMEFRAME_M3 - TIMEFRAME_M4 = MetaTrader5.TIMEFRAME_M4 - TIMEFRAME_M5 = MetaTrader5.TIMEFRAME_M5 - TIMEFRAME_M6 = MetaTrader5.TIMEFRAME_M6 - TIMEFRAME_M10 = MetaTrader5.TIMEFRAME_M10 - TIMEFRAME_M12 = MetaTrader5.TIMEFRAME_M10 - TIMEFRAME_M15 = MetaTrader5.TIMEFRAME_M15 - TIMEFRAME_M20 = MetaTrader5.TIMEFRAME_M20 - TIMEFRAME_M30 = MetaTrader5.TIMEFRAME_M30 - TIMEFRAME_H1 = MetaTrader5.TIMEFRAME_H1 - TIMEFRAME_H2 = MetaTrader5.TIMEFRAME_H2 - TIMEFRAME_H3 = MetaTrader5.TIMEFRAME_H3 - TIMEFRAME_H4 = MetaTrader5.TIMEFRAME_H4 - TIMEFRAME_H6 = MetaTrader5.TIMEFRAME_H6 - TIMEFRAME_H8 = MetaTrader5.TIMEFRAME_H8 - TIMEFRAME_H12 = MetaTrader5.TIMEFRAME_H12 - TIMEFRAME_D1 = MetaTrader5.TIMEFRAME_D1 - TIMEFRAME_W1 = MetaTrader5.TIMEFRAME_W1 - TIMEFRAME_MN1 = MetaTrader5.TIMEFRAME_MN1 - def __init__(self): # Connect to MetaTrader5. Opens if not already open. diff --git a/test/test_config.py b/test/test_config.py index 7c73c90..6ea8f24 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -4,13 +4,13 @@ from mt5_correlation.config import Config class TestConfig(unittest.TestCase): def test_load_and_get(self): - config = Config.instance() + config = Config() config.load("testconfig.yaml") val121 = config.get('test1.test1_2.val1_2_1') self.assertEqual(val121, 'val1_2_1', "Get returned incorrect value.") def test_set_and_save(self): - config = Config.instance() + config = Config() config.load("testconfig.yaml") path = 'test1.test1_2.val1_2_1' @@ -21,7 +21,7 @@ class TestConfig(unittest.TestCase): config.save() # Reopen config and get value to see if it is previously saved value - config = Config.instance() + config = Config() config.load("testconfig.yaml") saved_value = config.get(path) self.assertEqual(saved_value, 'newval', "New value was not saved and returned.") @@ -30,6 +30,20 @@ class TestConfig(unittest.TestCase): config.set(path, orig_value) config.save() + def test_get_root_nodes(self): + config = Config() + config.load("testconfig.yaml") + + # Get root nodes + root_nodes = config.get_root_nodes() + + # There should be 2 + self.assertTrue(len(root_nodes) == 2, "There should be 2 root nodes.") + + # The first should be test1 and the second should be test2 + self.assertEqual(root_nodes[0], 'test1', "First root node should be 'test1'.") + self.assertEqual(root_nodes[1], 'test2', "Second root node should be 'test2'.") + if __name__ == '__main__': unittest.main()