Split out config to seperate project (wxconfig). wxconfig now a dependency. Previous config code removed.

This commit is contained in:
Jamie Cash
2021-05-26 16:37:15 +01:00
parent 225f869276
commit c977e3ac1c
9 changed files with 134 additions and 618 deletions
+10 -51
View File
@@ -6,14 +6,14 @@ Calculates correlation coefficient between all symbols in MetaTrader5 Market Wat
2) Set up your python environment; and
3) Install the required libraries.
```
```shell
pip install -r mt5-correlation/requirements.txt
```
# Usage
If you set up a virtual environment in the Setup step, ensure this is activated. Then run the script.
```
```shell
python -m mt5_correlations/mt5_correlations.py
```
@@ -21,56 +21,15 @@ This will open a GUI.
## Calculating Baseline Coefficients
On the first time that you run, you will want to calculate the initial set of correlations.
1) Open settings and review the settings under the 'Calculate' tab. These settings are:
- from.days: The number of days of data to be used to calculate the coefficient.
- timeframe: The timeframe for price candles to use for the calculation. Possible values are:
* 1: 1 Minute candles
* 2: 2 Minute candles
* 3: 3 Minute candles
* 4: 4 Minute candles
* 5: 5 Minute candles
* 6: 6 Minute candles
* 10: 10 Minute candles
* 15: 15 Minute candles
* 20: 20 Minute candles
* 30: 30 Minute candles
* 16385: 1 Hour candles
* 16386: 2 Hour candles
* 16387: 3 Hour candles
* 16388: 4 Hour candles
* 16390: 6 Hour candles
* 16392: 8 Hour candles
* 16396: 12 Hour candles
* 16408: 1 Day candles
* 32769: 1 Week candles
* 49153: 1 Month candles
- min_prices: The minimum number of candles required to calculate a coefficient from. If any of the symbols do not have at least this number of candles then the coefficient won't be calculated.
- max_set_size_diff_pct: For a meaningful coefficient calculation, the two sets of data should be of a similar size. This setting specifies the % difference allowed for a correlation to be calculated. The smallest set of candle data must be at least this values % of the largest set.
- overlap_pct: The dates and times in the two sets of data must match. The coefficient will only be calculated against the dates that overlap. Any non overlapping dates will be discarded. This setting specifies the minimum size of the overlapping data when compared to the smallest set as a %. A coefficient will not be calculated if this threshold is not met.
- max_p_value: The maximum P value for the coefficient to be considered valid. A full explanation on the correlation coefficient P value is available here: https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.stats.pearsonr.html.
2) Set the threshold for the correlations to monitor. This can be set under the Settings 'Monitoring' tab and is named monitoring.threshold. Only settings with a coefficient over this threshold will be displayed and monitored.
3) Calculate the coefficients by selecting File/Calculate. All symbol pairs that have a correlation coefficient greater than the monitoring threshold will be displayed. A graph showing the price candle data used to calculate the coefficient for the pair will be displayed when you select the row.
1) Open settings and review the settings under the 'Calculate' tab. Hover over the individual settings for help.
2) Set the threshold for the correlations to monitor. This can be set under the Settings 'Monitoring' tab and is named 'Monitoring Threshold'. Only settings with a coefficient over this threshold will be displayed and monitored.
3) Calculate the coefficients by selecting File/Calculate. All symbol pairs that have a correlation coefficient greater than the monitoring threshold will be displayed. A graph showing the price candle data used to calculate the coefficient for the pair will be displayed when you select the row.
4) You may want to save. Select File/Save and choose a file name. This file will contain all the calculated coefficients, and the price data used to calculate them. This file can be loaded to avoid having to recalculate the baseline coefficients every time you use the application.
## Monitoring for Divergence
Once you have calculated or loaded the baseline coefficients, they can be monitored for divergence.
1) Open settings and review the settings under the 'Monitor' tab. These settings are:
- from.minutes: The number of minutes of data to be used to calculate the coefficient.
- interval: The number of seconds between monitoring events. The application will monitor for divergence every {interval} seconds.
- min_prices: Tick data will be converted to 1 second candles prior to calculation. This will enable data to be matched between symbols. This setting specifies the minimum number of price candles required to calculate a coefficient from. If any of the symbols do not have at least number of candles then the coefficient won't be calculated.
- max_set_size_diff_pct: For a meaningful coefficient calculation, the two sets of data should be of a similar size. This setting specifies the % difference allowed for a correlation to be calculated. The smallest set of price candles must be at least this values % of the largest set.
- overlap_pct: The dates and times in the two sets of data must match. The ticks will be converted to 1 second price candles before calculation. The coefficient will only be calculated against the times from the candles that overlap. Any non overlapping times will be discarded. This setting specifies the minimum size of the overlapping data when compared to the smallest set as a %. A coefficient will not be calculated if this threshold is not met.
- max_p_value: The maximum P value for the coefficient to be considered valid. A full explanation on the correlation coefficient P value is available here: https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.stats.pearsonr.html.
- monitoring_threshold: The application will only display correlated pairs that have a correlation coefficient greater than or equal to this value.
- divergence_threshold: The application will consider a pair to have diverged if the correlation coefficient falls below this threshold. These will be highlighted in yellow.
- tick_cache_time: Every calculation requires tick data for both symbols. Tick data will be cached for this number of seconds before being retrieved from MetaTrader. Some caching is recommended as a single monitoring run will request the same data for symbols that form multiple correlated pairs.
- autosave: Whether to auto save after every monitoring event. If a file was opened or has been saved, then the data will be saved to this file, otherwise the data will be saved to a file named autosave.cpd.
2) Switch the monitoring toggle to on. The application will continuously monitor for divergence. The data frame will be updated with the last time that the correlation was checked, and the last coefficient. The chart frame will contain 5 charts which will be updated after every monitoring event:
- 2 charts, one for each symbol in the correlated pair, showing the price data used to calculate the baseline coefficient.
- 2 charts, one for each symbol in the correlated pair, showing the tick data used to calculate the latest coefficient.
- 1 chart showing every correlation coefficient calculated for the symbol pair.
1) Open settings and review the settings under the 'Monitor' tab. Hover over the individual settings for help.
2) Switch the monitoring toggle to on. The application will continuously monitor for divergence. The data frame will be updated with the last time that the correlation was checked, and the last coefficient. The chart frame will contain 3 charts which will be updated after every monitoring event:
- One showing the price history data used to calculate the baseline coefficient for both symbols in the correlated pair;
- One showing the tick data used to calculate the latest coefficient for both symbols in the correlated pair.
- One showing every correlation coefficient calculated for the symbol pair.
+4 -4
View File
@@ -72,15 +72,15 @@ logging:
developer:
inspection: false
window:
x: 101
y: 9
x: 73
y: 0
width: 1510
height: 956
style: 541072960
settings_window:
x: 354
y: 299
width: 664
height: 317
width: 624
height: 328
style: 524352
...
+100
View File
@@ -0,0 +1,100 @@
---
calculate:
from:
days:
__label: Calculate from (days)
__helptext: The number of days of data to be used to calculate the coefficient.
timeframe:
__label: Timeframe
__helptext: The timeframe for price candles to use for the calculation. Possible values are 1=1 Minute candles; 2=2 Minute candles; 3=3 Minute candles; 4=4 Minute candles; 5=5 Minute candles; 6=6 Minute candles; 10=10 Minute candles; 15=15 Minute candles; 20=20 Minute candles; 30=30 Minute candles; 16385=1 Hour candles; 16386=2 Hour candles; 16387=3 Hour candles; 16388=4 Hour candles; 16390=6 Hour candles; 16392=8 Hour candles; 16396=12 Hour candles; 16408=1 Day candles; 32769=1 Week candles; or 49153=1 Month candles.
min_prices:
__label: Min Prices
__helptext: The minimum number of candles required to calculate a coefficient from. If any of the symbols do not have at least this number of candles then the coefficient won't be calculated.
max_set_size_diff_pct:
__label: Min Set Size Difference %
__helptext: For a meaningful coefficient calculation, the two sets of data should be of a similar size. This setting specifies the % difference allowed for a correlation to be calculated. The smallest set of candle data must be at least this values % of the largest set.
overlap_pct:
__label: Overlap %
__helptext: The dates and times in the two sets of data must match. The coefficient will only be calculated against the dates that overlap. Any non overlapping dates will be discarded. This setting specifies the minimum size of the overlapping data when compared to the smallest set as a %. A coefficient will not be calculated if this threshold is not met.
max_p_value:
__label: Max Pearsonr P Value
__helptext: The maximum P value for the coefficient to be considered valid. A full explanation on the correlation coefficient P value is available in the scipy pearsonr documentation.
monitor:
interval:
__label: Monitoring Interval
__helptext: The number of seconds between monitoring events.
calculations:
long:
__label: Long
__helptext: The settings for the correlation calculation using the longest timeframe.
from:
__label: From (Minutes)
__helptext: The number of minutes of data to be used to calculate the long coefficient.
min_prices:
__label: Min Prices
__helptext: Tick data will be converted to 1 second candles prior to calculation. This will enable data to be matched between symbols. This setting specifies the minimum number of price candles required to calculate a coefficient from. If any of the symbols do not have at least number of candles then the coefficient won't be calculated.
max_set_size_diff_pct:
__label: Max Set Size Difference %
__helptext: For a meaningful coefficient calculation, the two sets of data should be of a similar size. This setting specifies the % difference allowed for a correlation to be calculated. The smallest set of price candles must be at least this values % of the largest set.
overlap_pct:
__label: Overlap %
__helptext: The dates and times in the two sets of data must match. The ticks will be converted to 1 second price candles before calculation. The coefficient will only be calculated against the times from the candles that overlap. Any non overlapping times will be discarded. This setting specifies the minimum size of the overlapping data when compared to the smallest set as a %. A coefficient will not be calculated if this threshold is not met.
max_p_value:
__label: Max Pearsonr P Value
__helptext: The maximum P value for the coefficient to be considered valid. A full explanation on the correlation coefficient P value is available in the scipy pearsonr documentation.
medium:
__label: Medium
__helptext: The settings for the correlation calculation using a medium timeframe.
from:
__label: From (Minutes)
__helptext: The number of minutes of data to be used to calculate the medium coefficient.
min_prices:
__label: Min Prices
__helptext: Tick data will be converted to 1 second candles prior to calculation. This will enable data to be matched between symbols. This setting specifies the minimum number of price candles required to calculate a coefficient from. If any of the symbols do not have at least number of candles then the coefficient won't be calculated.
max_set_size_diff_pct:
__label: Max Set Size Difference %
__helptext: For a meaningful coefficient calculation, the two sets of data should be of a similar size. This setting specifies the % difference allowed for a correlation to be calculated. The smallest set of price candles must be at least this values % of the largest set.
overlap_pct:
__label: Overlap %
__helptext: The dates and times in the two sets of data must match. The ticks will be converted to 1 second price candles before calculation. The coefficient will only be calculated against the times from the candles that overlap. Any non overlapping times will be discarded. This setting specifies the minimum size of the overlapping data when compared to the smallest set as a %. A coefficient will not be calculated if this threshold is not met.
max_p_value:
__label: Max Pearsonr P Value
__helptext: The maximum P value for the coefficient to be considered valid. A full explanation on the correlation coefficient P value is available in the scipy pearsonr documentation.
short:
__label: Short
__helptext: The settings for the correlation calculation using the shortest timeframe.
from:
__label: From (Minutes)
__helptext: The number of minutes of data to be used to calculate the short coefficient.
min_prices:
__label: Min Prices
__helptext: Tick data will be converted to 1 second candles prior to calculation. This will enable data to be matched between symbols. This setting specifies the minimum number of price candles required to calculate a coefficient from. If any of the symbols do not have at least number of candles then the coefficient won't be calculated.
max_set_size_diff_pct:
__label: Max Set Size Difference %
__helptext: For a meaningful coefficient calculation, the two sets of data should be of a similar size. This setting specifies the % difference allowed for a correlation to be calculated. The smallest set of price candles must be at least this values % of the largest set.
overlap_pct:
__label: Overlap %
__helptext: The dates and times in the two sets of data must match. The ticks will be converted to 1 second price candles before calculation. The coefficient will only be calculated against the times from the candles that overlap. Any non overlapping times will be discarded. This setting specifies the minimum size of the overlapping data when compared to the smallest set as a %. A coefficient will not be calculated if this threshold is not met.
max_p_value:
__label: Max Pearsonr P Value
__helptext: The maximum P value for the coefficient to be considered valid. A full explanation on the correlation coefficient P value is available in the scipy pearsonr documentation.
monitoring_threshold:
__label: Monitoring Threshold
__helptext: Only pairs with a coefficient over this threshold will be displayed and monitored.
divergence_threshold:
__label: Divergence Threshold
__helptext: The application will consider a pair to have diverged if the correlation coefficient for all timeframes (long, medium and short) falls below this threshold.
monitor_inverse:
__label: Monitor Inverse
__helptext: Monitor Inverse Correlations (uses negative scale with -1 being fully inversly correlated)
tick_cache_time:
__label: Tick Cache Time
__helptext: Every calculation requires tick data for both symbols. Tick data will be cached for this number of seconds before being retrieved from MetaTrader. Some caching is recommended as a single monitoring run will request the same data for symbols that form multiple correlated pairs.
autosave:
__label: Auto Save
__helptext: Whether to auto save after every monitoring event. If a file was opened or has been saved, then the data will be saved to this file, otherwise the data will be saved to a file named autosave.cpd.
developer:
inspection:
__label: Inspection
__helptext: Provide GUI Inspection guidelines for developers modifying the GUI.
...
+4 -4
View File
@@ -4,7 +4,7 @@ Application to monitor previously correlated symbol pairs for correlation diverg
import definitions
import logging.config
from mt5_correlation.gui import MonitorFrame
from mt5_correlation.config import Config
import wxconfig as cfg
import wx
import wx.lib.mixins.inspection as wit
@@ -18,14 +18,14 @@ class InspectionApp(wx.App, wit.InspectionMixin):
if __name__ == "__main__":
# Load the config
Config().load(fr"{definitions.ROOT_DIR}\config.yaml")
cfg.Config().load(fr"{definitions.ROOT_DIR}\config.yaml", meta=fr"{definitions.ROOT_DIR}\configmeta.yaml")
# Get logging config and configure the logger
log_config = Config().get('logging')
log_config = cfg.Config().get('logging')
logging.config.dictConfig(log_config)
# Do we have inspection turned on. Create correct version of app
inspection = Config().get('developer.inspection')
inspection = cfg.Config().get('developer.inspection')
if inspection:
app = InspectionApp()
else:
-474
View File
@@ -1,474 +0,0 @@
import yaml
import wx
import logging
class Config(object):
"""
Provides access to application configuration parameters stored in config.yaml.
"""
config_filepath = None
__config = None
__instance = None
def __new__(cls):
"""
Singleton. Get instance of this class. Create if not already created.
:return:
"""
if cls.__instance is None:
cls.__instance = super(Config, cls).__new__(cls)
return cls.__instance
def load(self, path):
"""
Loads the applications config file
:param path: Path to config file
:return:
"""
with open(path, 'r') as yamlfile:
self.__config = yaml.safe_load(yamlfile)
# Store path so that we can save later
self.config_filepath = path
def save(self):
"""
Saves config file
:return:
"""
with open(self.config_filepath, 'w') as file:
file.write("---\n")
yaml.dump(self.__config, file, sort_keys=False)
file.write("...")
def get(self, path):
"""
Gets a config property value.
:param path: path to property. Path separated by .
:return: property value
"""
elements = path.split('.')
last = None
for element in elements:
if last is None:
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
:param path: path to property. Path separated by .
:param value: Value to set property to
:return:
"""
obj = self.__config
key_list = path.split(".")
for k in key_list[:-1]:
obj = obj[k]
obj[key_list[-1]] = value
class SettingsDialog(wx.Dialog):
"""
A dialog box for changing settings. A tab for each root node, with a tree view on left for every branch and a text
box for every value.
"""
# Settings
__settings = None # Will set in init.
# Store any settings that have changed
changed_settings = {}
def __init__(self, parent, exclude=None):
"""
Open the settings dialog
:param parent: The parent frame for this dialog
:param exclude: List of settings root nodes to exclude from this dialog
"""
# Super Constructor
wx.Dialog.__init__(self, parent=parent, id=wx.ID_ANY, title="Settings",
pos=wx.Point(x=Config().get('settings_window.x'),
y=Config().get('settings_window.y')),
size=wx.Size(width=Config().get('settings_window.width'),
height=Config().get('settings_window.height')),
style=Config().get('settings_window.style'))
self.SetTitle("Settings")
# Create logger and get config
self.__log = logging.getLogger(__name__)
self.__settings = Config()
# Dict of changes. Will commit only on ok
self.__changes = {}
# Dialog should be resizable
self.SetWindowStyle(wx.RESIZE_BORDER)
# Settings to exclude. Just settings_window if None. Add settings_window if not specified.
exclude = ['settings_window'] if exclude is None else exclude
if 'settings_window' not in exclude:
exclude.append('settings_window')
# We want 2 vertical sections, the tabbed notebook and the buttons. The buttons sizer will have 2 horizontal
# sections, one for each button.
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 = []
for node in root_nodes:
# Exclude?
if node not in exclude:
# Create new tab
self.__tabs.append(SettingsTab(self, self.__notebook, node))
# 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.
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)
# Bind window close event
self.Bind(wx.EVT_CLOSE, self.__on_close, self)
# Call on_page_select to select the first page
self.__on_page_select(event=None)
def __on_page_select(self, event):
# Call the tabs select method to populate
index = self.__notebook.GetSelection()
self.__tabs[index].select()
def __on_cancel(self, event):
# Clear changed settings and close
self.changed_settings = {}
self.EndModal(wx.ID_CANCEL)
self.Close()
def __on_ok(self, event):
# 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]
# 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. Note boolean needs to be handled differently as it
# doesn't cast directly.
if isinstance(orig_value, bool):
new_value = new_value.lower() in ['true', '1', 'yes', 't']
else:
new_value = type(orig_value)(new_value)
# 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:
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 __on_close(self, event):
# Save pos and size
x, y = self.GetPosition()
width, height = self.GetSize()
self.__settings.set('settings_window.x', x)
self.__settings.set('settings_window.y', y)
self.__settings.set('settings_window.width', width)
self.__settings.set('settings_window.height', height)
# Style
style = self.GetWindowStyle()
self.__settings.set('settings_window.style', style)
class SettingsTab(wx.Panel):
"""
A notebook tab containing the settings tree and values for a settings root node.
"""
# Root node for this tab
__root_node_name = None
# Parent frame. Set during constructor
__parent_frame = None
# Each tab has: a tree view; a values panel; and a list of value text boxes bound to a change
# event.
__tree = None
__tab_sizer = None
# Currently displayed value panel. Will be switched when tree menu items are selected.
__current_value_panel = None
def __init__(self, parent_frame, notebook, root_node):
"""
Creates a tab for the settings notebook.
:param parent_frame: The frame containing the notebook.
:param notebook. The notebook that this tab should be part of.
:param root_node. The root node name for the settings
"""
# Super Constructor
wx.Panel.__init__(self, parent=notebook)
# Store the parent frame and get the settings for this tab.
self.__parent_frame = parent_frame
# Store the root node for this tab
self.__root_node_name = root_node
# Create logger
self.__log = logging.getLogger(__name__)
# Build the tab and set it's sizer.
self.__tab_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.SetSizer(self.__tab_sizer)
# Create tree control and add it to sizer
self.__tree = SettingsTree(self, self.__root_node_name)
self.__tab_sizer.Add(self.__tree, 1, wx.ALL | wx.EXPAND, 1)
# Bind tree selection changed
self.__tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.__on_tree_select)
def select(self):
"""
To be called when this tab is selected. Populate value sizer for the selected item, If no item is selected,
populate for root.
:return:
"""
selected_item = self.__tree.GetSelection()
if selected_item.ID is None:
root_node = self.__tree.GetRootItem()
setting_path = self.__tree.GetItemData(root_node)
else:
setting_path = self.__tree.GetItemData(selected_item)
# Set the panel
self.__switch_value_panel(setting_path)
def __on_tree_select(self, event):
"""
Called when an item in the tree is selected. Displays the correct settings panel
:param event:
:return:
"""
# Get Selected item and check that it is a tree item
tree_item = event.GetItem()
if not tree_item.IsOk():
return
# Get the setting path from item data
setting_path = self.__tree.GetItemData(tree_item)
# Switch the panel
self.__switch_value_panel(setting_path)
def __switch_value_panel(self, setting_path):
"""
Switched the value panel to the correct one for the settings path
:param setting_path:
:return:
"""
# Get current panel and delete.
if self.__current_value_panel is not None:
self.__current_value_panel.Destroy()
# Create the new value panel and add to sizer.
self.__current_value_panel = SettingsValuePanel(self.__parent_frame, self, setting_path)
self.__tab_sizer.Add(self.__current_value_panel, 1, wx.ALL | wx.EXPAND, 1)
# Redraw
self.__tab_sizer.Layout()
class SettingsTree(wx.TreeCtrl):
"""
A Tree control containing the settings nodes settings node
"""
__root_node_name = None
def __init__(self, settings_tab, settings_node):
"""
Creates a tree control for specified settings node.
:param settings_tab. The settings_tab on which this tree control should be displayed.
:param settings_node. The node name for the settings who's values will be presented
"""
# Super Constructor
wx.TreeCtrl.__init__(self, parent=settings_tab)
# Set root node
self.__root_node_name = settings_node
# Build the tree
self.__build_tree(None, self.__root_node_name)
# Expand it
# self.ExpandAll()
# Set max size. Width should be best size width, height should be auto (-1)
best_width = self.GetBestSize()[0] * 2 # Hack, best size not working
self.SetMaxSize((best_width, -1))
def __build_tree(self, node, node_name):
"""
Recursive function to build the tree and value panels from the node using the settings
:param node: The tree view node to build from. If none, builds from root.
:param node_name. The name of the node to build from.
"""
# Build root node if node is None
if node is None:
node = self.AddRoot(self.__root_node_name)
self.SetItemData(node, self.__root_node_name)
# Get settings
node_path = self.GetItemData(node)
settings = Config().get(node_path)
# Iterate settings, adding branches. Recurse to add sub branches.
for setting in settings:
# Get settings path
settings_path = f"{self.GetItemData(node)}.{setting}"
# 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 = self.AppendItem(node, setting)
self.SetItemData(node_id, settings_path)
# Recurse
self.__build_tree(node_id, value)
class SettingsValuePanel(wx.ScrolledWindow):
"""
A panel containing text boxes for editing values for a settings node
"""
__value_sizer = None
__value_boxes = []
def __init__(self, parent_frame, settings_tab, node):
"""
Creates a panel containing values for a settings node.
:param parent_frame: The frame containing the notebook.
:param settings_tab. The settings_tab on which this panel should be displayed.
:param node. The node name for the settings who's values will be presented
"""
# Super Constructor
wx.ScrolledWindow.__init__(self, parent=settings_tab)
# Create logger
self.__log = logging.getLogger(__name__)
# Store the parent frame and get the settings for this node.
self.__parent_frame = parent_frame
self.__settings = Config().get(node)
leaf_settings = {}
for setting in self.__settings:
if type(self.__settings[setting]) is not dict:
leaf_settings[setting] = self.__settings[setting]
# Add the value sizer for settings values.
self.__value_sizer = wx.FlexGridSizer(rows=len(leaf_settings), cols=2, vgap=2, hgap=2)
# Add the sizer to the panel
self.SetSizer(self.__value_sizer)
# Display every value
for setting in leaf_settings:
# Setting path
setting_path = f"{node}.{setting}"
# Value. Make sure that we display changed value if already changed
if setting_path in self.__parent_frame.changed_settings:
value = self.__parent_frame.changed_settings[setting_path]
else:
value = leaf_settings[setting]
# Add a label and value text box
label = wx.StaticText(self, wx.ID_ANY, setting, style=wx.ALIGN_LEFT)
self.__value_boxes.append(wx.TextCtrl(self, wx.ID_ANY, f"{value}", style=wx.ALIGN_LEFT))
self.__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=setting_path))
# Layout the value sizer
self.__value_sizer.Layout()
# Setup scrollbars
self.SetScrollbars(1, 1, 1000, 1000)
def __get_on_change_evt_handler(self, setting_path):
"""
Returns a new event handler with a parameter of the settings path
:param setting_path:
:return:
"""
def on_value_changed(event):
old_val = self.__settings.get(setting_path)
self.__parent_frame.changed_settings[setting_path] = event.String
self.__log.debug(f"Value changed from {old_val} to {self.__parent_frame.changed_settings[setting_path]} "
f"for {setting_path}.")
return on_value_changed
+14 -14
View File
@@ -7,7 +7,7 @@ import matplotlib
import matplotlib.ticker as mticker
from mt5_correlation import correlation as cor
from mt5_correlation.config import Config, SettingsDialog
import wxconfig as cfg
from datetime import datetime, timedelta
import pytz
import pandas as pd
@@ -40,15 +40,15 @@ class MonitorFrame(wx.Frame):
def __init__(self):
# Super
wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title="Divergence Monitor",
pos=wx.Point(x=Config().get('window.x'),
y=Config().get('window.y')),
size=wx.Size(width=Config().get('window.width'),
height=Config().get('window.height')),
style=Config().get('window.style'))
pos=wx.Point(x=cfg.Config().get('window.x'),
y=cfg.Config().get('window.y')),
size=wx.Size(width=cfg.Config().get('window.width'),
height=cfg.Config().get('window.height')),
style=cfg.Config().get('window.style'))
# Create logger and get config
self.__log = logging.getLogger(__name__)
self.__config = Config()
self.__config = cfg.Config()
# Create correlation instance to maintain state of calculated coefficients. Set min coefficient from config
self.__cor = cor.Correlation(monitoring_threshold=self.__config.get("monitor.monitoring_threshold"),
@@ -292,7 +292,7 @@ class MonitorFrame(wx.Frame):
Opens the settings dialog
:return:
"""
settings_dialog = SettingsDialog(parent=self, exclude=['window'])
settings_dialog = cfg.SettingsDialog(parent=self, exclude=['window'])
res = settings_dialog.ShowModal()
if res == wx.ID_OK:
# Reload relevant parts of app
@@ -347,7 +347,7 @@ class MonitorFrame(wx.Frame):
if reload_logger:
self.__log.info("Settings updated. Reloading logger.")
log_config = Config().get('logging')
log_config = cfg.Config().get('logging')
logging.config.dictConfig(log_config)
if reload_graph:
@@ -474,7 +474,7 @@ class DataTable(wx.grid.GridTableBase):
self.data = data
# Get divergence threshold from app config
self.divergence_threshold = Config().get('monitor.divergence_threshold')
self.divergence_threshold = cfg.Config().get('monitor.divergence_threshold')
def GetNumberRows(self):
return len(self.data)
@@ -505,7 +505,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 = Config().get('monitor.divergence_threshold')
threshold = cfg.Config().get('monitor.divergence_threshold')
if col in [MonitorFrame.COLUMN_STATUS]:
# Is status one of interest
value = self.GetValue(row, col)
@@ -687,9 +687,9 @@ class GraphPanel(wx.Panel):
ax.set_xticklabels([])
# Legend
ax.legend([f"{Config().get('monitor.calculations.long.from')} Minutes",
f"{Config().get('monitor.calculations.medium.from')} Minutes",
f"{Config().get('monitor.calculations.short.from')} Minutes"])
ax.legend([f"{cfg.Config().get('monitor.calculations.long.from')} Minutes",
f"{cfg.Config().get('monitor.calculations.medium.from')} Minutes",
f"{cfg.Config().get('monitor.calculations.short.from')} Minutes"])
# Lines showing divergence threshold. 2 if we are monitoring inverse correlations.
if divergence_threshold is not None:
+2 -2
View File
@@ -4,7 +4,7 @@ MetaTrader5==5.0.34
pytz==2021.1
scipy==1.6.0
logging==0.4.9.6
pyyaml==5.4.1
wxpython==4.1.1
mock==4.0.3
numpy==1.20.0
numpy==1.20.0
wxconfig==1.2
-49
View File
@@ -1,49 +0,0 @@
import unittest
from mt5_correlation.config import Config
class TestConfig(unittest.TestCase):
def test_load_and_get(self):
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()
config.load("testconfig.yaml")
path = 'test1.test1_2.val1_2_1'
# Save new value, storing orig so we can restore later
orig_value = config.get(path)
config.set(path, "newval")
config.save()
# Reopen config and get value to see if it is previously saved value
config = Config()
config.load("testconfig.yaml")
saved_value = config.get(path)
self.assertEqual(saved_value, 'newval', "New value was not saved and returned.")
# Restore and save file
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()
-20
View File
@@ -1,20 +0,0 @@
---
test1:
test1_1:
val1_1_1: val1_1_1
val1_1_2: val1_1_2
val1_1_3: val1_1_3
test1_2:
val1_2_1: val1_2_1
val1_2_2: val1_2_2
val1_2_3: val1_2_3
test2:
test2_1:
val2_1_1: val1_1_1
val2_1_2: val1_1_2
val2_1_3: val1_1_3
test2_2:
val2_2_1: val1_2_1
val2_2_2: val1_2_2
val2_2_3: val1_2_3
...