Added settings dialog
This commit is contained in:
+38
-1
@@ -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
|
||||
...
|
||||
@@ -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
@@ -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()
|
||||
|
||||
+24
-20
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+305
-70
@@ -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
|
||||
|
||||
+23
-23
@@ -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.
|
||||
|
||||
|
||||
+17
-3
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user