From d08fef0cbb5b6ae8a45f0be320f5008a46d55c42 Mon Sep 17 00:00:00 2001 From: Jamie Cash Date: Mon, 8 Mar 2021 17:50:24 +0000 Subject: [PATCH] Added support for multiple timeframes for coefficient calculation. --- config.yaml | 11 +- mt5_correlation/correlation.py | 186 ++++++++++++++++++++------------- mt5_correlation/gui.py | 71 ++++++++----- test/test_correlation.py | 22 ++-- 4 files changed, 177 insertions(+), 113 deletions(-) diff --git a/config.yaml b/config.yaml index 819a52b..1dc01ce 100644 --- a/config.yaml +++ b/config.yaml @@ -8,8 +8,11 @@ calculate: overlap_pct: 90 max_p_value: 0.05 monitor: - from: - minutes: 30 + calculate_from: + long: + minutes: 30 + short: + minutes: 10 interval: 10 min_prices: 400 max_set_size_diff_pct: 90 @@ -58,8 +61,8 @@ logging: developer: inspection: false window: - x: 42 - y: 51 + x: 37 + y: 42 width: 1512 height: 975 style: 541072960 diff --git a/mt5_correlation/correlation.py b/mt5_correlation/correlation.py index 1c4a45d..55b9f45 100644 --- a/mt5_correlation/correlation.py +++ b/mt5_correlation/correlation.py @@ -36,7 +36,12 @@ class Correlation: # The price data used to calculate the correlations __price_data = None - # Coefficient data and history. Will be created as dataframes in Init + # The shortest timeframe (which is the largest value) for calculate_from. This will be used when we update + # the coefficient_data dataframe. All calculations for all values specified in calculate_from will be stored in + # coefficient_history, however only the shortest timeframe will be updated in coefficient_data. + __shortest_timeframe = None + + # Coefficient data and history. Will be created in init call to __reset_coefficient_data coefficient_data = None coefficient_history = None @@ -179,7 +184,7 @@ class Correlation: # If we were monitoring, we stopped, so start again. if was_monitoring: self.start_monitor(interval=self.__monitoring_params['interval'], - from_mins=self.__monitoring_params['from_mins'], + calculate_from=self.__monitoring_params['calculate_from'], min_prices=self.__monitoring_params['min_prices'], max_set_size_diff_pct=self.__monitoring_params['max_set_size_diff_pct'], overlap_pct=self.__monitoring_params['overlap_pct'], @@ -197,14 +202,15 @@ class Correlation: return price_data - def start_monitor(self, interval, from_mins, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90, - max_p_value=0.05, cache_time=10, autosave=False, filename='autosave.cpd'): + def start_monitor(self, interval, calculate_from, min_prices=100, max_set_size_diff_pct=90, + overlap_pct=90, max_p_value=0.05, cache_time=10, autosave=False, filename='autosave.cpd'): """ Starts monitor to continuously update the coefficient for all symbol pairs in that meet the min_coefficient threshold. :param interval: How often to check in seconds - :param from_mins: The number of minutes of tick data to use for calculations + :param calculate_from: The number of minutes of tick data to use for calculation. This can be a single value or + a list. If a list, then calculations will be performed for every from date in list. :param min_prices: The minimum number of prices that should be used to calculate coefficient. If this threshold is not met then returned coefficient will be None :param max_set_size_diff_pct: Correlations will only be calculated if the sizes of the two price data sets are @@ -228,13 +234,19 @@ class Correlation: self.__log.debug(f"Starting monitor.") self.__monitoring = True + # Store the shortest timeframe (which is the largest value) for calculate_from. This will be used when we update + # the coefficient_data dataframe. All calculations for all values specified in calculate_from will be stored in + # coefficient_history, however only the shortest timeframe will be updated in coefficient_data. + self.__shortest_timeframe = min(calculate_from) if isinstance(calculate_from, list) else calculate_from + # 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. Store the params. We will need to use these if we have # to stop and restart the monitor. Note, this happens during calculate - self.__monitoring_params = {'interval': interval, 'from_mins': from_mins, - 'min_prices': min_prices, 'max_set_size_diff_pct': max_set_size_diff_pct, - 'overlap_pct': overlap_pct, 'max_p_value': max_p_value, 'cache_time': cache_time, - 'autosave': autosave, 'filename': filename} + self.__monitoring_params = {'interval': interval, 'calculate_from': calculate_from, + 'min_prices': min_prices, + 'max_set_size_diff_pct': max_set_size_diff_pct, 'overlap_pct': overlap_pct, + 'max_p_value': max_p_value, 'cache_time': cache_time, 'autosave': autosave, + 'filename': filename} thread = threading.Thread(target=self.__monitor, kwargs=self.__monitoring_params) thread.start() @@ -306,16 +318,23 @@ class Correlation: return coefficient - def get_coefficient_history(self, symbol1, symbol2): + def get_coefficient_history(self, symbol1, symbol2, timeframe=None): """ Returns the coefficient history for the specified symbol pair calculated during this instance. Coefficient history does not persist between instances. :param symbol1: :param symbol2: + :param timeframe: Only return history for the specified timeframe. If None, this is ignored and all history + records are returned for the specified symbols :return: dataframe containing history of coefficient data. """ history = self.coefficient_history[(self.coefficient_history['Symbol 1'] == symbol1) & (self.coefficient_history['Symbol 2'] == symbol2)] + + # If calculate from was specified, filter on it. + if timeframe is not None: + history = history[(history['Timeframe'] == timeframe)] + return history def get_ticks(self, symbol, date_from=None, date_to=None, cache_time=0, cache_only=False): @@ -355,14 +374,15 @@ class Correlation: self.__log.debug(f"Ticks for {symbol} retrieved from source and cached.") return ticks - def __monitor(self, interval, from_mins, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90, - max_p_value=0.05, cache_time=10, autosave=False, filename='autosave.cpd'): + def __monitor(self, interval, calculate_from, min_prices=100, max_set_size_diff_pct=90, + overlap_pct=90, max_p_value=0.05, cache_time=10, autosave=False, filename='autosave.cpd'): """ The actual monitor method. Private. This should not be called outside of this class. Use start_monitoring and stop_monitoring. :param interval: How often to check in seconds - :param from_mins: The number of minutes of tick data to use for calculations + :param calculate_from: The number of minutes of tick data to use for calculation. This can be a single value or + a list. If a list, then calculations will be performed for every from date in list. :param min_prices: The minimum number of prices that should be used to calculate coefficient. If this threshold is not met then returned coefficient will be None :param max_set_size_diff_pct: Correlations will only be calculated if the sizes of the two price data sets are @@ -382,19 +402,19 @@ class Correlation: # Only run if monitor is not stopped if self.__monitoring: # Update all coefficients - self.__update_all_coefficients(from_mins=from_mins, min_prices=min_prices, - max_set_size_diff_pct=max_set_size_diff_pct, overlap_pct=overlap_pct, - max_p_value=max_p_value, cache_time=cache_time) + self.__update_all_coefficients(calculate_from=calculate_from, + min_prices=min_prices, max_set_size_diff_pct=max_set_size_diff_pct, + overlap_pct=overlap_pct, max_p_value=max_p_value, cache_time=cache_time) # Autosave if autosave: self.save(filename=filename) # Schedule the timer to run again - params = {'interval': interval, 'from_mins': from_mins, 'min_prices': min_prices, - 'max_set_size_diff_pct': max_set_size_diff_pct, 'overlap_pct': overlap_pct, - 'max_p_value': max_p_value, "cache_time": cache_time, 'autosave': autosave, - 'filename': filename} + params = {'interval': interval, 'calculate_from': calculate_from, + 'min_prices': min_prices, 'max_set_size_diff_pct': max_set_size_diff_pct, + 'overlap_pct': overlap_pct, 'max_p_value': max_p_value, "cache_time": cache_time, + 'autosave': autosave, 'filename': filename} self.__scheduler.enter(delay=interval, priority=1, action=self.__monitor, kwargs=params) # Log the stack. Debug stack overflow @@ -405,13 +425,14 @@ class Correlation: self.__first_run = False self.__scheduler.run() - def __update_coefficient(self, symbol1, symbol2, from_mins, min_prices=100, max_set_size_diff_pct=90, - overlap_pct=90, max_p_value=0.05, cache_time=10): + def __update_coefficients(self, symbol1, symbol2, calculate_from, min_prices=100, + max_set_size_diff_pct=90, overlap_pct=90, max_p_value=0.05, cache_time=10): """ - Updates the coefficient for the specified symbol pair + Updates the long and short coefficients for the specified symbol pair :param symbol1: Name of symbol to calculate coefficient for. :param symbol2: Name of symbol to calculate coefficient for. - :param from_mins: The number of minutes of tick data to use for calculations + :param calculate_from: The number of minutes of tick data to use for calculation. This can be a single value or + a list. If a list, then calculations will be performed for every from date in list. :param min_prices: The minimum number of prices that should be used to calculate coefficient. If this threshold is not met then returned coefficient will be None :param max_set_size_diff_pct: Correlations will only be calculated if the sizes of the two price data sets are @@ -423,61 +444,72 @@ class Correlation: :return: correlation coefficient, or None if coefficient could not be calculated. """ - coefficient = None + # Convert calculate from to list of one if only one value is provided + if not isinstance(calculate_from, list): + calculate_from = [calculate_from, ] # Get dates - # From and to dates for calculations. + # From and to dates for calculations. From should be furthest away if list is provided in calculate_from timezone = pytz.timezone("Etc/UTC") date_to = datetime.now(tz=timezone) - date_from = date_to - timedelta(minutes=from_mins) + date_from = date_to - timedelta(minutes=max(calculate_from)) - # Get the tick data + # Get the tick data for the longest timeframe calculation. We will extract shorter timeframes from it if list + # was provided in calculate_from to avoid retrieving multiple times. symbol1ticks = self.get_ticks(symbol=symbol1, date_from=date_from, date_to=date_to, cache_time=cache_time) symbol2ticks = self.get_ticks(symbol=symbol2, date_from=date_from, date_to=date_to, cache_time=cache_time) - # Resample to 1 sec OHLC, this will help with coefficient calculation ensuring that we dont have more than one - # tick per second and ensuring that times can match. We will need to set the index to time for the resample - # then revert back to a 'time' column. We will then need to remove rows with nan in 'close' price - if symbol1ticks is not None and symbol2ticks is not None and \ - len(symbol1ticks.index) > 0 and len(symbol2ticks.index) > 0: - symbol1ticks = symbol1ticks.set_index('time') - symbol2ticks = symbol2ticks.set_index('time') + # Resample to 1 sec OHLC, this will help with coefficient calculation ensuring that we dont have more than + # one tick per second and ensuring that times can match. We will need to set the index to time for the + # resample then revert back to a 'time' column. We will then need to remove rows with nan in 'close' price + if symbol1ticks is not None and symbol2ticks is not None and len(symbol1ticks.index) > 0 and \ + len(symbol2ticks.index) > 0: + try: - symbol1prices = symbol1ticks['ask'].resample('1S').ohlc() - symbol2prices = symbol2ticks['ask'].resample('1S').ohlc() + symbol1ticks = symbol1ticks.set_index('time') + symbol2ticks = symbol2ticks.set_index('time') + s1_prices = symbol1ticks['ask'].resample('1S').ohlc() + s2_prices = symbol2ticks['ask'].resample('1S').ohlc() except RecursionError: - self.__log.warning(f"Coefficient could not be calculated for {symbol1}:{symbol2} as prices could not " + self.__log.warning(f"Coefficient could not be calculated for {symbol1}:{symbol2}. prices could not " f"be resampled.") else: - symbol1prices.reset_index(inplace=True) - symbol2prices.reset_index(inplace=True) - symbol1prices = symbol1prices[symbol1prices['close'].notna()] - symbol2prices = symbol2prices[symbol2prices['close'].notna()] + s1_prices.reset_index(inplace=True) + s2_prices.reset_index(inplace=True) + s1_prices = s1_prices[s1_prices['close'].notna()] + s2_prices = s2_prices[s2_prices['close'].notna()] - # Calculate the coefficient - coefficient = self.calculate_coefficient(symbol1_prices=symbol1prices, symbol2_prices=symbol2prices, - min_prices=min_prices, - max_set_size_diff_pct=max_set_size_diff_pct, - overlap_pct=overlap_pct, max_p_value=max_p_value) + # Calculate for all timeframes + for from_mins in calculate_from: + # Get the from date as a datetime64 + date_from_subset = pd.Timestamp(date_to - timedelta(minutes=from_mins)).to_datetime64() - self.__log.debug(f"Symbol pair {symbol1}:{symbol2} has a coefficient of {coefficient}.") - else: - coefficient = None + # Get subset of the price data + s1_prices_subset = s1_prices[(s1_prices['time'] >= date_from_subset)] + s2_prices_subset = s2_prices[(s2_prices['time'] >= date_from_subset)] - # Update the coefficient data - if coefficient is not None: - self.__update_coefficient_data(symbol1=symbol1, symbol2=symbol2, coefficient=coefficient, - date_from=date_from, date_to=date_to) + # Calculate the coefficient + coefficient = \ + self.calculate_coefficient(symbol1_prices=s1_prices_subset, symbol2_prices=s2_prices_subset, + min_prices=min_prices, max_set_size_diff_pct=max_set_size_diff_pct, + overlap_pct=overlap_pct, max_p_value=max_p_value) - return coefficient + self.__log.debug(f"Symbol pair {symbol1}:{symbol2} has a coefficient of {coefficient} for last " + f"{from_mins} minutes.") - def __update_all_coefficients(self, from_mins, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90, - max_p_value=0.05, cache_time=10): + # Update the coefficient data + if coefficient is not None: + self.__update_coefficient_data(symbol1=symbol1, symbol2=symbol2, coefficient=coefficient, + timeframe=from_mins, date_from=date_from_subset, date_to=date_to) + + def __update_all_coefficients(self, calculate_from, min_prices=100, max_set_size_diff_pct=90, + overlap_pct=90, max_p_value=0.05, cache_time=10): """ Updates the coefficient for all symbol pairs in that meet the min_coefficient threshold. Symbol pairs that meet the threshold can be accessed through the filtered_coefficient_data property. - :param from_mins: The number of minutes of tick data to use for calculations + :param calculate_from: The number of minutes of tick data to use for calculation. This can be a single value or + a list. If a list, then calculations will be performed for every from date in list. :param min_prices: The minimum number of prices that should be used to calculate coefficient. If this threshold is not met then returned coefficient will be None :param max_set_size_diff_pct: Correlations will only be calculated if the sizes of the two price data sets are @@ -493,36 +525,37 @@ class Correlation: for index, row in self.filtered_coefficient_data.iterrows(): symbol1 = row['Symbol 1'] symbol2 = row['Symbol 2'] - self.__update_coefficient(symbol1=symbol1, symbol2=symbol2, from_mins=from_mins, - min_prices=min_prices, max_set_size_diff_pct=max_set_size_diff_pct, - overlap_pct=overlap_pct, max_p_value=max_p_value, cache_time=cache_time) + self.__update_coefficients(symbol1=symbol1, symbol2=symbol2, calculate_from=calculate_from, + min_prices=min_prices, max_set_size_diff_pct=max_set_size_diff_pct, + overlap_pct=overlap_pct, max_p_value=max_p_value, cache_time=cache_time) def __reset_coefficient_data(self): """ Clears coefficient data and history. :return: """ - # Create dataframe for coefficient data + # Create dataframes for coefficient data. coefficient_data_columns = ['Symbol 1', 'Symbol 2', 'Base Coefficient', 'UTC Date From', 'UTC Date To', 'Timeframe', 'Last Check', 'Last Coefficient'] self.coefficient_data = pd.DataFrame(columns=coefficient_data_columns) - # Create dataframe for coefficient history - coefficient_history_columns = ['Symbol 1', 'Symbol 2', 'Coefficient', 'UTC Date From', 'UTC Date To'] + # Create dataframes for coefficient history. + coefficient_history_columns = ['Symbol 1', 'Symbol 2', 'Coefficient', 'Timeframe', 'Date From', 'Date To'] self.coefficient_history = pd.DataFrame(columns=coefficient_history_columns) # Clear price data and tick data self.__price_data = None self.__monitor_tick_data = {} - def __update_coefficient_data(self, symbol1, symbol2, coefficient, date_from, date_to): + def __update_coefficient_data(self, symbol1, symbol2, coefficient, timeframe, date_from, date_to): """ Updates the coefficient data with the latest coefficient and adds to coefficient history. :param symbol1: :param symbol2: - :param coefficient: - :param date_from: - :param date_to: + :param coefficient: The coefficient calculated + :param timeframe: The number of minutes of price data used to calculate the coefficient + :param date_from: The date from for which the coefficient was calculated + :param date_to: The date from for which the coefficient was calculated :return: """ @@ -531,14 +564,17 @@ class Correlation: # Update data if we have a coefficient and add to history if coefficient is not None: - self.coefficient_data.loc[(self.coefficient_data['Symbol 1'] == symbol1) & - (self.coefficient_data['Symbol 2'] == symbol2), - 'Last Check'] = now + # The coefficient data table is only updated for the shortest calculation timeframe. + if timeframe == self.__shortest_timeframe: + self.coefficient_data.loc[(self.coefficient_data['Symbol 1'] == symbol1) & + (self.coefficient_data['Symbol 2'] == symbol2), + 'Last Check'] = now - self.coefficient_data.loc[(self.coefficient_data['Symbol 1'] == symbol1) & - (self.coefficient_data['Symbol 2'] == symbol2), - 'Last Coefficient'] = coefficient + self.coefficient_data.loc[(self.coefficient_data['Symbol 1'] == symbol1) & + (self.coefficient_data['Symbol 2'] == symbol2), + 'Last Coefficient'] = coefficient + # However the history data is always updated row = pd.DataFrame(columns=self.coefficient_history.columns, - data=[[symbol1, symbol2, coefficient, date_from, date_to]]) + data=[[symbol1, symbol2, coefficient, timeframe, date_from, date_to]]) self.coefficient_history = self.coefficient_history.append(row) diff --git a/mt5_correlation/gui.py b/mt5_correlation/gui.py index 44fdf01..e9721a7 100644 --- a/mt5_correlation/gui.py +++ b/mt5_correlation/gui.py @@ -284,7 +284,8 @@ class MonitorFrame(wx.Frame): filename = self.__opened_filename if self.__opened_filename is not None else 'autosave.cpd' self.__cor.start_monitor(interval=self.__config.get('monitor.interval'), - from_mins=self.__config.get('monitor.from.minutes'), + calculate_from=[self.__config.get('monitor.calculate_from.long.minutes'), + self.__config.get('monitor.calculate_from.short.minutes')], min_prices=self.__config.get('monitor.min_prices'), max_set_size_diff_pct=self.__config.get('monitor.max_set_size_diff_pct'), overlap_pct=self.__config.get('monitor.overlap_pct'), @@ -329,7 +330,7 @@ class MonitorFrame(wx.Frame): reload_correlations = True if setting.startswith('logging.'): reload_logger = True - if setting.startswith('monitor.from.'): + if setting.startswith('monitor.calculate_from'): reload_graph = True # Now perform the actions @@ -337,7 +338,8 @@ class MonitorFrame(wx.Frame): self.__log.info("Settings updated. Reloading monitoring timer.") self.__cor.stop_monitor() self.__cor.start_monitor(interval=self.__config.get('monitor.interval'), - from_mins=self.__config.get('monitor.from.minutes'), + calculate_from=[self.__config.get('monitor.calculate_from.long.minutes'), + self.__config.get('monitor.calculate_from.short.minutes')], min_prices=self.__config.get('monitor.min_prices'), max_set_size_diff_pct=self.__config.get('monitor.max_set_size_diff_pct'), overlap_pct=self.__config.get('monitor.overlap_pct'), @@ -418,14 +420,16 @@ class MonitorFrame(wx.Frame): symbol_2_price_data = self.__cor.get_price_data(symbol2) symbol_1_ticks = self.__cor.get_ticks(symbol1, cache_only=True) symbol_2_ticks = self.__cor.get_ticks(symbol2, cache_only=True) - history_data = self.__cor.get_coefficient_history(symbol1, symbol2) - times = history_data['UTC Date To'] - coefficients = history_data['Coefficient'] - + history_data_short = \ + self.__cor.get_coefficient_history(symbol1, symbol2, + self.__config.get('monitor.calculate_from.short.minutes')) + history_data_long = \ + self.__cor.get_coefficient_history(symbol1, symbol2, + self.__config.get('monitor.calculate_from.long.minutes')) # Display if we have any data self.__log.debug(f"Refreshing history graph {symbol1}:{symbol2}.") - self.__graph.draw(times=times, coefficients=coefficients, prices=[symbol_1_price_data, symbol_2_price_data], - ticks=[symbol_1_ticks, symbol_2_ticks], symbols=[symbol1, symbol2]) + self.__graph.draw(prices=[symbol_1_price_data, symbol_2_price_data], ticks=[symbol_1_ticks, symbol_2_ticks], + history=[history_data_short, history_data_long], symbols=[symbol1, symbol2]) # Un-hide and layout if hidden if not self.__graph.IsShown(): @@ -434,7 +438,7 @@ class MonitorFrame(wx.Frame): def __timer_event(self, event): """ - Called on timer event. Refreshes grid and updatates selected graph. + Called on timer event. Refreshes grid and updates selected graph. :return: """ self.refresh_grid() @@ -486,7 +490,7 @@ class DataTable(wx.grid.GridTableBase): # If column is last coefficient, get value and check against threshold. Highlight if diverged. threshold = Config().get('monitor.divergence_threshold') - if col == MonitorFrame.COLUMN_LAST_COEFFICIENT: + if col in [MonitorFrame.COLUMN_LAST_COEFFICIENT]: value = self.GetValue(row, col) if value != "": value = float(value) @@ -531,26 +535,39 @@ class GraphPanel(wx.Panel): self.__axes = None self.__fig = None - def draw(self, times, coefficients, prices=None, symbols=None, ticks=None): + def draw(self, prices=None, ticks=None, history=None, symbols=None): """ Plot the correlations. - :param times: Series of time values for x axis for coefficient history chart - :param coefficients: Series of coefficients values for y axis of coefficient history chart :param prices: Price data used to calculate base coefficient. List [Symbol1 Price Data, Symbol 2 Price Data] - :param symbols: Symbols. List [Symbol1, Symbol2] :param ticks: Ticks used to calculate last coefficient. List [Symbol1, Symbol2] + :param history: Coefficient history data. List of data for one or more timeframes. + :param symbols: Symbols. List [Symbol1, Symbol2] :return: """ + + # Get all plots for history. History can contain multiple plots for different timeframes. They will all be + # plotted on the same chart. + times = [] + coefficients = [] + for hist in history: + times.append(hist['Date To']) + coefficients.append(hist['Coefficient']) + # Clear. We will need to redraw for ax in self.__axes: ax.clear() if symbols is not None and len(symbols) == 2: # Axis ranges - price_chart_date_range = [min(min(prices[0]['time']), min(prices[1]['time'])), - max(max(prices[0]['time']), max(prices[1]['time']))] - tick_chart_date_range = [min(min(ticks[0]['time']), min(ticks[1]['time'])), - max(max(ticks[0]['time']), max(ticks[1]['time']))] + default_range = [datetime.now() - timedelta(days=1), datetime.now()] + price_chart_date_range = default_range # We need a default here if no data is available + if prices is not None and len(prices) == 0 and prices[0] is not None and prices[1] is not None: + price_chart_date_range = [min(min(prices[0]['time']), min(prices[1]['time'])), + max(max(prices[0]['time']), max(prices[1]['time']))] + tick_chart_date_range = default_range # We need a default here if no data is available + if ticks is not None and len(ticks) == 0 and ticks[0] is not None and ticks[1] is not None: + tick_chart_date_range = [min(min(ticks[0]['time']), min(ticks[1]['time'])), + max(max(ticks[0]['time']), max(ticks[1]['time']))] # Chart config titles = [f"Base Coefficient Price Data for {symbols[0]}", f"Base Coefficient Price Data for {symbols[1]}", @@ -560,7 +577,7 @@ class GraphPanel(wx.Panel): ylims = [None, None, None, None, [-1, 1]] xlabels = [None, None, None, None, None] ylabels = ['Price', 'Price', 'Price', 'Price', 'Coefficient'] - tick_labels = [[], prices[1]['time'], [], ticks[1]['time'], times] + tick_labels = [[], prices[1]['time'], [], ticks[1]['time'], times[0]] mtick_fmts = [None, self.__tick_fmt_date, None, self.__tick_fmt_time, self.__tick_fmt_time] mtick_rot = [0, 45, 0, 45, 45] xdata = [prices[0]['time'], prices[1]['time'], ticks[0]['time'], ticks[1]['time'], times] @@ -591,11 +608,15 @@ class GraphPanel(wx.Panel): self.__axes[index].spines["top"].set_visible(False) self.__axes[index].spines["right"].set_visible(False) - # Plot - if types[index] == 'plot': - self.__axes[index].plot(xdata[index], ydata[index]) - elif types[index] == 'scatter': - self.__axes[index].scatter(xdata[index], ydata[index], s=1) + # Plot. There may be more than one set of data or each chart. Convert single data to list, then loop + xdata_list = xdata[index] if isinstance(xdata[index], list) else [xdata[index], ] + ydata_list = ydata[index] if isinstance(ydata[index], list) else [ydata[index], ] + + for data_index in range(0, len(xdata_list)): + if types[index] == 'plot': + self.__axes[index].plot(xdata_list[data_index], ydata_list[data_index]) + elif types[index] == 'scatter': + self.__axes[index].scatter(xdata_list[data_index], ydata_list[data_index], s=1) # Layout with padding between charts self.__fig.tight_layout(pad=0.5) diff --git a/test/test_correlation.py b/test/test_correlation.py index cab52dc..80490b8 100644 --- a/test/test_correlation.py +++ b/test/test_correlation.py @@ -198,11 +198,11 @@ class TestCorrelation(unittest.TestCase): # Patch it in mock.copy_ticks_range.side_effect = [tick_data_s1, tick_data_s3, tick_data_s4] - # Start the monitor. Run every second. Use ~10 seconds of data. Were not testing the overlap and price data - # quality metrics here as that is set elsewhere so these can be set to not take effect. Set cache level high - # and don't use autosave. Timer runs in a separate thread so test can continue after it has started. - cor.start_monitor(interval=1, from_mins=0.66, min_prices=0, max_set_size_diff_pct=0, overlap_pct=0, - max_p_value=1, cache_time=100, autosave=False) + # Start the monitor. Run every second. Use ~10 and ~5 seconds of data. Were not testing the overlap and price + # data quality metrics here as that is set elsewhere so these can be set to not take effect. Set cache level + # high and don't use autosave. Timer runs in a separate thread so test can continue after it has started. + cor.start_monitor(interval=1, calculate_from=[0.66, 0.33], min_prices=0, max_set_size_diff_pct=0, + overlap_pct=0, max_p_value=1, cache_time=100, autosave=False) # Wait 2 seconds so timer runs twice time.sleep(2) @@ -210,8 +210,12 @@ class TestCorrelation(unittest.TestCase): # Stop the monitor cor.stop_monitor() - # We should have 2 coefficients calculated for each symbol pair - self.assertEqual(len(cor.coefficient_history.index), 6) + # We should have 2 coefficients calculated for each symbol pair for each date_from value, so 12 in total. + self.assertEqual(len(cor.coefficient_history.index), 12) + + # We should have 2 coefficients calculated for a single symbol pair and timeframe + self.assertEqual(len(cor.get_coefficient_history('SYMBOL1', 'SYMBOL2', 0.66)), 2, + "We should have 2 history records for SYMBOL1:SYMBOL2 using the 0.66 min timeframe.") @patch('mt5_correlation.mt5.MetaTrader5') def test_load_and_save(self, mock): @@ -246,8 +250,8 @@ class TestCorrelation(unittest.TestCase): # Start monitor and run for a seconds with a 1 second interval to produce some coefficient history. Then stop # the monitor - cor.start_monitor(interval=1, from_mins=0.66, min_prices=0, max_set_size_diff_pct=0, overlap_pct=0, - max_p_value=1, cache_time=100, autosave=False) + cor.start_monitor(interval=1, calculate_from=0.66, min_prices=0, max_set_size_diff_pct=0, + overlap_pct=0, max_p_value=1, cache_time=100, autosave=False) time.sleep(2) cor.stop_monitor()