Added settings dialog

This commit is contained in:
Jamie Cash
2021-02-16 17:15:00 +00:00
parent 15eec4d08b
commit 2349a362e8
8 changed files with 425 additions and 170 deletions
+38 -1
View File
@@ -9,11 +9,48 @@ calculate:
max_p_value: 0.05 max_p_value: 0.05
monitor: monitor:
from: from:
minutes: 10 minutes: 15
interval: 10 interval: 10
min_prices: 400 min_prices: 400
max_set_size_diff_pct: 50 max_set_size_diff_pct: 50
overlap_pct: 50 overlap_pct: 50
max_p_value: 0.05 max_p_value: 0.05
monitoring_threshold: 0.9
divergence_threshold: 0.8 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
... ...
-37
View File
@@ -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
+6 -8
View File
@@ -9,17 +9,15 @@ from mt5_correlation.config import Config
import wx import wx
if __name__ == "__main__": 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 # 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 # Start the app
app = wx.App(False) app = wx.App(False)
frame = MonitorFrame(None, wx.ID_ANY, "") frame = MonitorFrame()
frame.Show() frame.Show()
app.MainLoop() app.MainLoop()
+24 -20
View File
@@ -7,25 +7,18 @@ class Config(object):
Provides access to application configuration parameters stored in config.yaml. Provides access to application configuration parameters stored in config.yaml.
""" """
_config = None config_filepath = None
_path = None __config = None
_instance = None __instance = None
def __init__(self): def __new__(cls):
"""
Singleton. Raise runtime error
"""
raise RuntimeError('Call instance() instead')
@classmethod
def instance(cls):
""" """
Singleton. Get instance of this class. Create if not already created. Singleton. Get instance of this class. Create if not already created.
:return: :return:
""" """
if cls._instance is None: if cls.__instance is None:
cls._instance = cls.__new__(cls) cls.__instance = super(Config, cls).__new__(cls)
return cls._instance return cls.__instance
def load(self, path): def load(self, path):
""" """
@@ -34,10 +27,10 @@ class Config(object):
:return: :return:
""" """
with open(path, 'r') as yamlfile: 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 # Store path so that we can save later
self._path = path self.config_filepath = path
def save(self): def save(self):
""" """
@@ -45,9 +38,9 @@ class Config(object):
:return: :return:
""" """
with open(self._path, 'w') as file: with open(self.config_filepath, 'w') as file:
file.write("---\n") file.write("---\n")
yaml.dump(self._config, file, sort_keys=False) yaml.dump(self.__config, file, sort_keys=False)
file.write("...") file.write("...")
def get(self, path): def get(self, path):
@@ -62,12 +55,23 @@ class Config(object):
for element in elements: for element in elements:
if last is None: if last is None:
last = self._config[element] last = self.__config[element]
else: else:
last = last[element] last = last[element]
return last 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): def set(self, path, value):
""" """
Sets a config property value Sets a config property value
@@ -75,7 +79,7 @@ class Config(object):
:param value: Value to set property to :param value: Value to set property to
:return: :return:
""" """
obj = self._config obj = self.__config
key_list = path.split(".") key_list = path.split(".")
for k in key_list[:-1]: for k in key_list[:-1]:
+12 -8
View File
@@ -16,8 +16,12 @@ class Correlation:
A class to maintain the state of the calculated correlation coefficients. A class to maintain the state of the calculated correlation coefficients.
""" """
min_coefficient = 0.9 # Minimum base coefficient for monitoring. Symbol pairs with a lower correlation
monitoring = False # Monitoring cor correlations # 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): def __init__(self):
self.log = logging.getLogger(__name__) self.log = logging.getLogger(__name__)
@@ -221,7 +225,7 @@ class Correlation:
:return: :return:
""" """
self.log.debug(f"Starting monitor.") 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 # 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 # keep scheduling itself while self.monitoring is True
@@ -237,7 +241,7 @@ class Correlation:
:return: :return:
""" """
self.log.debug(f"Stopping monitor.") 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, 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): max_p_value=0.05):
@@ -257,10 +261,10 @@ class Correlation:
:return: correlation coefficient, or None if coefficient could not be calculated. :return: correlation coefficient, or None if coefficient could not be calculated.
:return: :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 # Only run if monitor is not stopped
if self.monitoring: if self._monitoring:
# Update all coefficients # Update all coefficients
self.update_all_coefficients(date_from=date_from, date_to=date_to, min_prices=min_prices, 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, max_set_size_diff_pct=max_set_size_diff_pct, overlap_pct=overlap_pct,
@@ -276,10 +280,10 @@ class Correlation:
@property @property
def filtered_coefficient_data(self): 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: 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: else:
return None return None
+305 -70
View File
@@ -1,13 +1,12 @@
import wx import wx
import wx.grid import wx.grid
import wx.lib.masked as masked
from mt5_correlation.correlation import Correlation from mt5_correlation.correlation import Correlation
from mt5_correlation.config import Config from mt5_correlation.config import Config
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pytz import pytz
import pandas as pd import pandas as pd
import logging import logging
import definitions import logging.config
class MonitorFrame(wx.Frame): class MonitorFrame(wx.Frame):
@@ -27,35 +26,33 @@ class MonitorFrame(wx.Frame):
COLUMN_LAST_CHECK = 7 COLUMN_LAST_CHECK = 7
COLUMN_LAST_COEFFICIENT = 8 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.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() self.cor = Correlation()
self.cor.monitoring_threshold = self.config.get("monitor.monitoring_threshold")
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 # Status bar
self.statusbar = self.CreateStatusBar(1) self.statusbar = self.CreateStatusBar(1)
# Menu Bar # Menu Bar and file menu
self.menubar = wx.MenuBar() self.menubar = wx.MenuBar()
file_menu = wx.Menu() 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_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_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.") menu_item_saveas = file_menu.Append(wx.ID_ANY, "Save As", "Save correlations file.")
# Calculate
file_menu.AppendSeparator() file_menu.AppendSeparator()
menu_item_calculate = file_menu.Append(wx.ID_ANY, "Calculate", "Calculate base coefficients.") menu_item_calculate = file_menu.Append(wx.ID_ANY, "Calculate", "Calculate base coefficients.")
file_menu.AppendSeparator()
# Close menu_item_settings = file_menu.Append(wx.ID_ANY, "Settings", "Change application settings.")
file_menu.AppendSeparator() file_menu.AppendSeparator()
menu_item_exit = file_menu.Append(wx.ID_ANY, "Exit", "Close the application") 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.menubar.Append(file_menu, "File")
self.SetMenuBar(self.menubar) self.SetMenuBar(self.menubar)
# Main window # Main window. We want 2 horizontal sections, the grid showing correlations and a graph. In the correlations
self.window = wx.SplitterWindow(self, wx.ID_ANY) # section, we want 2 vertical sections, the monitor toggle and the correlations grid. For the toggle we want 2
self.window.SetMinimumPaneSize(20) # 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) # Create the correlations grid. This is a data table using pandas dataframe for underlying data. Add the
# correlations_grid to the correlations sizer.
# 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.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.SetTable(self.table, takeOwnership=True)
self.grid_correlations.EnableEditing(0) self.grid_correlations.EnableEditing(False)
self.grid_correlations.EnableDragRowSize(0) self.grid_correlations.EnableDragRowSize(False)
self.grid_correlations.EnableDragGridSize(0) self.grid_correlations.EnableDragColSize(False)
self.grid_correlations.EnableDragGridSize(False)
self.grid_correlations.SetSelectionMode(wx.grid.Grid.SelectRows) 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_INDEX, 0) # Index. Hide
self.grid_correlations.SetColSize(self.COLUMN_SYMBOL1, 100) # Symbol 1 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_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_TIMEFRAME, 0) # Timeframe. Hide.
self.grid_correlations.SetColSize(self.COLUMN_LAST_CHECK, 100) # Last Check 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.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 # Add the correlations sizer and the charts to the main sizer.
sizer_chart = wx.BoxSizer(wx.VERTICAL) 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) # Size the window.
sizer_chart.Add(label_2, 0, wx.ALIGN_CENTER_HORIZONTAL, 0) self.SetSize((800, 500))
# 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() self.Layout()
# Set up timer to refresh grid # Set up timer to refresh grid
self.timer = wx.Timer(self) self.timer = wx.Timer(self)
# Bind my buttons, timer, menu # Bind monitor button
self.filter_button.Bind(wx.EVT_BUTTON, self.change_min_coefficient)
self.monitor_toggle.Bind(wx.EVT_TOGGLEBUTTON, self.monitor) self.monitor_toggle.Bind(wx.EVT_TOGGLEBUTTON, self.monitor)
# Bind timer
self.Bind(wx.EVT_TIMER, self.refresh_grid, self.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.open_file, menu_item_open)
self.Bind(wx.EVT_MENU, self.save_file, menu_item_save) 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.save_file_as, menu_item_saveas)
self.Bind(wx.EVT_MENU, self.calculate_coefficients, menu_item_calculate) 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) self.Bind(wx.EVT_MENU, self.quit, menu_item_exit)
# Bind window close event # Bind window close event
@@ -197,10 +199,6 @@ class MonitorFrame(wx.Frame):
def quit(self, event): def quit(self, event):
self.Close() 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): def refresh_grid(self, event):
""" """
Refreshes grid. Notifies if rows have been added or deleted. 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 # Check state of toggle button. If on, then start monitoring, else stop
if self.monitor_toggle.GetValue(): if self.monitor_toggle.GetValue():
self.log.debug("Starting monitoring.") self.log.debug("Starting monitoring.")
self.monitor_toggle.SetBackgroundColour(wx.GREEN)
self.monitor_toggle.SetLabelText("On")
self.SetStatusText("Monitoring for changes to coefficients.") self.SetStatusText("Monitoring for changes to coefficients.")
# Calculate correlations fro last 10 mins # Calculate correlations fro last 10 mins
@@ -265,10 +265,28 @@ class MonitorFrame(wx.Frame):
max_p_value=self.config.get('monitor.max_p_value')) max_p_value=self.config.get('monitor.max_p_value'))
else: else:
self.log.debug("Stopping monitoring.") self.log.debug("Stopping monitoring.")
self.monitor_toggle.SetBackgroundColour(wx.RED)
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):
"""
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): def on_close(self, event):
""" """
Window closing. Save coefficients and stop monitoring. Window closing. Save coefficients and stop monitoring.
@@ -295,11 +313,7 @@ class DataTable(wx.grid.GridTableBase):
self.data = data self.data = data
# Get divergence threshold from app config # Get divergence threshold from app config
# Get application config self.divergence_threshold = Config().get('monitor.divergence_threshold')
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): def GetNumberRows(self):
return len(self.data) return len(self.data)
@@ -330,7 +344,7 @@ class DataTable(wx.grid.GridTableBase):
attr = wx.grid.GridCellAttr() attr = wx.grid.GridCellAttr()
# If column is last coefficient, get value and check against threshold. Highlight if diverged. # 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: if col == MonitorFrame.COLUMN_LAST_COEFFICIENT:
value = self.GetValue(row, col) value = self.GetValue(row, col)
if value != "": if value != "":
@@ -341,3 +355,224 @@ class DataTable(wx.grid.GridTableBase):
attr.SetBackgroundColour(wx.WHITE) attr.SetBackgroundColour(wx.WHITE)
return attr 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
+23 -23
View File
@@ -2,35 +2,35 @@ import pandas as pd
import MetaTrader5 import MetaTrader5
import logging 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: class MT5:
""" """
A class to connect to and interface with MetaTrader 5 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): def __init__(self):
# Connect to MetaTrader5. Opens if not already open. # Connect to MetaTrader5. Opens if not already open.
+17 -3
View File
@@ -4,13 +4,13 @@ from mt5_correlation.config import Config
class TestConfig(unittest.TestCase): class TestConfig(unittest.TestCase):
def test_load_and_get(self): def test_load_and_get(self):
config = Config.instance() config = Config()
config.load("testconfig.yaml") config.load("testconfig.yaml")
val121 = config.get('test1.test1_2.val1_2_1') val121 = config.get('test1.test1_2.val1_2_1')
self.assertEqual(val121, 'val1_2_1', "Get returned incorrect value.") self.assertEqual(val121, 'val1_2_1', "Get returned incorrect value.")
def test_set_and_save(self): def test_set_and_save(self):
config = Config.instance() config = Config()
config.load("testconfig.yaml") config.load("testconfig.yaml")
path = 'test1.test1_2.val1_2_1' path = 'test1.test1_2.val1_2_1'
@@ -21,7 +21,7 @@ class TestConfig(unittest.TestCase):
config.save() config.save()
# Reopen config and get value to see if it is previously saved value # Reopen config and get value to see if it is previously saved value
config = Config.instance() config = Config()
config.load("testconfig.yaml") config.load("testconfig.yaml")
saved_value = config.get(path) saved_value = config.get(path)
self.assertEqual(saved_value, 'newval', "New value was not saved and returned.") 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.set(path, orig_value)
config.save() 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__': if __name__ == '__main__':
unittest.main() unittest.main()