Added support for multiple timeframes for coefficient calculation.

This commit is contained in:
Jamie Cash
2021-03-08 17:50:24 +00:00
parent 15f1ca2288
commit d08fef0cbb
4 changed files with 177 additions and 113 deletions
+7 -4
View File
@@ -8,8 +8,11 @@ calculate:
overlap_pct: 90 overlap_pct: 90
max_p_value: 0.05 max_p_value: 0.05
monitor: monitor:
from: calculate_from:
minutes: 30 long:
minutes: 30
short:
minutes: 10
interval: 10 interval: 10
min_prices: 400 min_prices: 400
max_set_size_diff_pct: 90 max_set_size_diff_pct: 90
@@ -58,8 +61,8 @@ logging:
developer: developer:
inspection: false inspection: false
window: window:
x: 42 x: 37
y: 51 y: 42
width: 1512 width: 1512
height: 975 height: 975
style: 541072960 style: 541072960
+111 -75
View File
@@ -36,7 +36,12 @@ class Correlation:
# The price data used to calculate the correlations # The price data used to calculate the correlations
__price_data = None __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_data = None
coefficient_history = None coefficient_history = None
@@ -179,7 +184,7 @@ class Correlation:
# If we were monitoring, we stopped, so start again. # If we were monitoring, we stopped, so start again.
if was_monitoring: if was_monitoring:
self.start_monitor(interval=self.__monitoring_params['interval'], 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'], min_prices=self.__monitoring_params['min_prices'],
max_set_size_diff_pct=self.__monitoring_params['max_set_size_diff_pct'], max_set_size_diff_pct=self.__monitoring_params['max_set_size_diff_pct'],
overlap_pct=self.__monitoring_params['overlap_pct'], overlap_pct=self.__monitoring_params['overlap_pct'],
@@ -197,14 +202,15 @@ class Correlation:
return price_data return price_data
def start_monitor(self, interval, from_mins, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90, def start_monitor(self, interval, calculate_from, min_prices=100, max_set_size_diff_pct=90,
max_p_value=0.05, cache_time=10, autosave=False, filename='autosave.cpd'): 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 Starts monitor to continuously update the coefficient for all symbol pairs in that meet the min_coefficient
threshold. threshold.
:param interval: How often to check in seconds :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 :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 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 :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.__log.debug(f"Starting monitor.")
self.__monitoring = True 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 # 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 # 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 # to stop and restart the monitor. Note, this happens during calculate
self.__monitoring_params = {'interval': interval, 'from_mins': from_mins, self.__monitoring_params = {'interval': interval, 'calculate_from': calculate_from,
'min_prices': min_prices, 'max_set_size_diff_pct': max_set_size_diff_pct, 'min_prices': min_prices,
'overlap_pct': overlap_pct, 'max_p_value': max_p_value, 'cache_time': cache_time, 'max_set_size_diff_pct': max_set_size_diff_pct, 'overlap_pct': overlap_pct,
'autosave': autosave, 'filename': filename} '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 = threading.Thread(target=self.__monitor, kwargs=self.__monitoring_params)
thread.start() thread.start()
@@ -306,16 +318,23 @@ class Correlation:
return coefficient 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. Returns the coefficient history for the specified symbol pair calculated during this instance.
Coefficient history does not persist between instances. Coefficient history does not persist between instances.
:param symbol1: :param symbol1:
:param symbol2: :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. :return: dataframe containing history of coefficient data.
""" """
history = self.coefficient_history[(self.coefficient_history['Symbol 1'] == symbol1) & history = self.coefficient_history[(self.coefficient_history['Symbol 1'] == symbol1) &
(self.coefficient_history['Symbol 2'] == symbol2)] (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 return history
def get_ticks(self, symbol, date_from=None, date_to=None, cache_time=0, cache_only=False): 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.") self.__log.debug(f"Ticks for {symbol} retrieved from source and cached.")
return ticks return ticks
def __monitor(self, interval, from_mins, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90, def __monitor(self, interval, calculate_from, min_prices=100, max_set_size_diff_pct=90,
max_p_value=0.05, cache_time=10, autosave=False, filename='autosave.cpd'): 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 The actual monitor method. Private. This should not be called outside of this class. Use start_monitoring and
stop_monitoring. stop_monitoring.
:param interval: How often to check in seconds :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 :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 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 :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 # Only run if monitor is not stopped
if self.__monitoring: if self.__monitoring:
# Update all coefficients # Update all coefficients
self.__update_all_coefficients(from_mins=from_mins, min_prices=min_prices, self.__update_all_coefficients(calculate_from=calculate_from,
max_set_size_diff_pct=max_set_size_diff_pct, overlap_pct=overlap_pct, min_prices=min_prices, max_set_size_diff_pct=max_set_size_diff_pct,
max_p_value=max_p_value, cache_time=cache_time) overlap_pct=overlap_pct, max_p_value=max_p_value, cache_time=cache_time)
# Autosave # Autosave
if autosave: if autosave:
self.save(filename=filename) self.save(filename=filename)
# Schedule the timer to run again # Schedule the timer to run again
params = {'interval': interval, 'from_mins': from_mins, 'min_prices': min_prices, params = {'interval': interval, 'calculate_from': calculate_from,
'max_set_size_diff_pct': max_set_size_diff_pct, 'overlap_pct': overlap_pct, 'min_prices': min_prices, 'max_set_size_diff_pct': max_set_size_diff_pct,
'max_p_value': max_p_value, "cache_time": cache_time, 'autosave': autosave, 'overlap_pct': overlap_pct, 'max_p_value': max_p_value, "cache_time": cache_time,
'filename': filename} 'autosave': autosave, 'filename': filename}
self.__scheduler.enter(delay=interval, priority=1, action=self.__monitor, kwargs=params) self.__scheduler.enter(delay=interval, priority=1, action=self.__monitor, kwargs=params)
# Log the stack. Debug stack overflow # Log the stack. Debug stack overflow
@@ -405,13 +425,14 @@ class Correlation:
self.__first_run = False self.__first_run = False
self.__scheduler.run() self.__scheduler.run()
def __update_coefficient(self, symbol1, symbol2, from_mins, min_prices=100, max_set_size_diff_pct=90, def __update_coefficients(self, symbol1, symbol2, calculate_from, min_prices=100,
overlap_pct=90, max_p_value=0.05, cache_time=10): 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 symbol1: Name of symbol to calculate coefficient for.
:param symbol2: 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 :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 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 :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. :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 # 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") timezone = pytz.timezone("Etc/UTC")
date_to = datetime.now(tz=timezone) 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) 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) 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 # Resample to 1 sec OHLC, this will help with coefficient calculation ensuring that we dont have more than
# tick per second and ensuring that times can match. We will need to set the index to time for the resample # one tick per second and ensuring that times can match. We will need to set the index to time for the
# then revert back to a 'time' column. We will then need to remove rows with nan in 'close' price # 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 \ if symbol1ticks is not None and symbol2ticks is not None and len(symbol1ticks.index) > 0 and \
len(symbol1ticks.index) > 0 and len(symbol2ticks.index) > 0: len(symbol2ticks.index) > 0:
symbol1ticks = symbol1ticks.set_index('time')
symbol2ticks = symbol2ticks.set_index('time')
try: try:
symbol1prices = symbol1ticks['ask'].resample('1S').ohlc() symbol1ticks = symbol1ticks.set_index('time')
symbol2prices = symbol2ticks['ask'].resample('1S').ohlc() symbol2ticks = symbol2ticks.set_index('time')
s1_prices = symbol1ticks['ask'].resample('1S').ohlc()
s2_prices = symbol2ticks['ask'].resample('1S').ohlc()
except RecursionError: 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.") f"be resampled.")
else: else:
symbol1prices.reset_index(inplace=True) s1_prices.reset_index(inplace=True)
symbol2prices.reset_index(inplace=True) s2_prices.reset_index(inplace=True)
symbol1prices = symbol1prices[symbol1prices['close'].notna()] s1_prices = s1_prices[s1_prices['close'].notna()]
symbol2prices = symbol2prices[symbol2prices['close'].notna()] s2_prices = s2_prices[s2_prices['close'].notna()]
# Calculate the coefficient # Calculate for all timeframes
coefficient = self.calculate_coefficient(symbol1_prices=symbol1prices, symbol2_prices=symbol2prices, for from_mins in calculate_from:
min_prices=min_prices, # Get the from date as a datetime64
max_set_size_diff_pct=max_set_size_diff_pct, date_from_subset = pd.Timestamp(date_to - timedelta(minutes=from_mins)).to_datetime64()
overlap_pct=overlap_pct, max_p_value=max_p_value)
self.__log.debug(f"Symbol pair {symbol1}:{symbol2} has a coefficient of {coefficient}.") # Get subset of the price data
else: s1_prices_subset = s1_prices[(s1_prices['time'] >= date_from_subset)]
coefficient = None s2_prices_subset = s2_prices[(s2_prices['time'] >= date_from_subset)]
# Update the coefficient data # Calculate the coefficient
if coefficient is not None: coefficient = \
self.__update_coefficient_data(symbol1=symbol1, symbol2=symbol2, coefficient=coefficient, self.calculate_coefficient(symbol1_prices=s1_prices_subset, symbol2_prices=s2_prices_subset,
date_from=date_from, date_to=date_to) min_prices=min_prices, max_set_size_diff_pct=max_set_size_diff_pct,
overlap_pct=overlap_pct, max_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, # Update the coefficient data
max_p_value=0.05, cache_time=10): 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 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. 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 :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 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 :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(): for index, row in self.filtered_coefficient_data.iterrows():
symbol1 = row['Symbol 1'] symbol1 = row['Symbol 1']
symbol2 = row['Symbol 2'] symbol2 = row['Symbol 2']
self.__update_coefficient(symbol1=symbol1, symbol2=symbol2, from_mins=from_mins, 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, 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) overlap_pct=overlap_pct, max_p_value=max_p_value, cache_time=cache_time)
def __reset_coefficient_data(self): def __reset_coefficient_data(self):
""" """
Clears coefficient data and history. Clears coefficient data and history.
:return: :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', coefficient_data_columns = ['Symbol 1', 'Symbol 2', 'Base Coefficient', 'UTC Date From', 'UTC Date To',
'Timeframe', 'Last Check', 'Last Coefficient'] 'Timeframe', 'Last Check', 'Last Coefficient']
self.coefficient_data = pd.DataFrame(columns=coefficient_data_columns) self.coefficient_data = pd.DataFrame(columns=coefficient_data_columns)
# Create dataframe for coefficient history # Create dataframes for coefficient history.
coefficient_history_columns = ['Symbol 1', 'Symbol 2', 'Coefficient', 'UTC Date From', 'UTC Date To'] coefficient_history_columns = ['Symbol 1', 'Symbol 2', 'Coefficient', 'Timeframe', 'Date From', 'Date To']
self.coefficient_history = pd.DataFrame(columns=coefficient_history_columns) self.coefficient_history = pd.DataFrame(columns=coefficient_history_columns)
# Clear price data and tick data # Clear price data and tick data
self.__price_data = None self.__price_data = None
self.__monitor_tick_data = {} 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. Updates the coefficient data with the latest coefficient and adds to coefficient history.
:param symbol1: :param symbol1:
:param symbol2: :param symbol2:
:param coefficient: :param coefficient: The coefficient calculated
:param date_from: :param timeframe: The number of minutes of price data used to calculate the coefficient
:param date_to: :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: :return:
""" """
@@ -531,14 +564,17 @@ class Correlation:
# Update data if we have a coefficient and add to history # Update data if we have a coefficient and add to history
if coefficient is not None: if coefficient is not None:
self.coefficient_data.loc[(self.coefficient_data['Symbol 1'] == symbol1) & # The coefficient data table is only updated for the shortest calculation timeframe.
(self.coefficient_data['Symbol 2'] == symbol2), if timeframe == self.__shortest_timeframe:
'Last Check'] = now 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.loc[(self.coefficient_data['Symbol 1'] == symbol1) &
(self.coefficient_data['Symbol 2'] == symbol2), (self.coefficient_data['Symbol 2'] == symbol2),
'Last Coefficient'] = coefficient 'Last Coefficient'] = coefficient
# However the history data is always updated
row = pd.DataFrame(columns=self.coefficient_history.columns, 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) self.coefficient_history = self.coefficient_history.append(row)
+46 -25
View File
@@ -284,7 +284,8 @@ class MonitorFrame(wx.Frame):
filename = self.__opened_filename if self.__opened_filename is not None else 'autosave.cpd' filename = self.__opened_filename if self.__opened_filename is not None else 'autosave.cpd'
self.__cor.start_monitor(interval=self.__config.get('monitor.interval'), 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'), min_prices=self.__config.get('monitor.min_prices'),
max_set_size_diff_pct=self.__config.get('monitor.max_set_size_diff_pct'), max_set_size_diff_pct=self.__config.get('monitor.max_set_size_diff_pct'),
overlap_pct=self.__config.get('monitor.overlap_pct'), overlap_pct=self.__config.get('monitor.overlap_pct'),
@@ -329,7 +330,7 @@ class MonitorFrame(wx.Frame):
reload_correlations = True reload_correlations = True
if setting.startswith('logging.'): if setting.startswith('logging.'):
reload_logger = True reload_logger = True
if setting.startswith('monitor.from.'): if setting.startswith('monitor.calculate_from'):
reload_graph = True reload_graph = True
# Now perform the actions # Now perform the actions
@@ -337,7 +338,8 @@ class MonitorFrame(wx.Frame):
self.__log.info("Settings updated. Reloading monitoring timer.") self.__log.info("Settings updated. Reloading monitoring timer.")
self.__cor.stop_monitor() self.__cor.stop_monitor()
self.__cor.start_monitor(interval=self.__config.get('monitor.interval'), 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'), min_prices=self.__config.get('monitor.min_prices'),
max_set_size_diff_pct=self.__config.get('monitor.max_set_size_diff_pct'), max_set_size_diff_pct=self.__config.get('monitor.max_set_size_diff_pct'),
overlap_pct=self.__config.get('monitor.overlap_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_2_price_data = self.__cor.get_price_data(symbol2)
symbol_1_ticks = self.__cor.get_ticks(symbol1, cache_only=True) symbol_1_ticks = self.__cor.get_ticks(symbol1, cache_only=True)
symbol_2_ticks = self.__cor.get_ticks(symbol2, cache_only=True) symbol_2_ticks = self.__cor.get_ticks(symbol2, cache_only=True)
history_data = self.__cor.get_coefficient_history(symbol1, symbol2) history_data_short = \
times = history_data['UTC Date To'] self.__cor.get_coefficient_history(symbol1, symbol2,
coefficients = history_data['Coefficient'] 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 # Display if we have any data
self.__log.debug(f"Refreshing history graph {symbol1}:{symbol2}.") 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], self.__graph.draw(prices=[symbol_1_price_data, symbol_2_price_data], ticks=[symbol_1_ticks, symbol_2_ticks],
ticks=[symbol_1_ticks, symbol_2_ticks], symbols=[symbol1, symbol2]) history=[history_data_short, history_data_long], symbols=[symbol1, symbol2])
# Un-hide and layout if hidden # Un-hide and layout if hidden
if not self.__graph.IsShown(): if not self.__graph.IsShown():
@@ -434,7 +438,7 @@ class MonitorFrame(wx.Frame):
def __timer_event(self, event): 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: :return:
""" """
self.refresh_grid() 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. # If column is last coefficient, get value and check against threshold. Highlight if diverged.
threshold = Config().get('monitor.divergence_threshold') threshold = Config().get('monitor.divergence_threshold')
if col == MonitorFrame.COLUMN_LAST_COEFFICIENT: if col in [MonitorFrame.COLUMN_LAST_COEFFICIENT]:
value = self.GetValue(row, col) value = self.GetValue(row, col)
if value != "": if value != "":
value = float(value) value = float(value)
@@ -531,26 +535,39 @@ class GraphPanel(wx.Panel):
self.__axes = None self.__axes = None
self.__fig = 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. 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 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 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: :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 # Clear. We will need to redraw
for ax in self.__axes: for ax in self.__axes:
ax.clear() ax.clear()
if symbols is not None and len(symbols) == 2: if symbols is not None and len(symbols) == 2:
# Axis ranges # Axis ranges
price_chart_date_range = [min(min(prices[0]['time']), min(prices[1]['time'])), default_range = [datetime.now() - timedelta(days=1), datetime.now()]
max(max(prices[0]['time']), max(prices[1]['time']))] price_chart_date_range = default_range # We need a default here if no data is available
tick_chart_date_range = [min(min(ticks[0]['time']), min(ticks[1]['time'])), if prices is not None and len(prices) == 0 and prices[0] is not None and prices[1] is not None:
max(max(ticks[0]['time']), max(ticks[1]['time']))] 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 # Chart config
titles = [f"Base Coefficient Price Data for {symbols[0]}", f"Base Coefficient Price Data for {symbols[1]}", 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]] ylims = [None, None, None, None, [-1, 1]]
xlabels = [None, None, None, None, None] xlabels = [None, None, None, None, None]
ylabels = ['Price', 'Price', 'Price', 'Price', 'Coefficient'] 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_fmts = [None, self.__tick_fmt_date, None, self.__tick_fmt_time, self.__tick_fmt_time]
mtick_rot = [0, 45, 0, 45, 45] mtick_rot = [0, 45, 0, 45, 45]
xdata = [prices[0]['time'], prices[1]['time'], ticks[0]['time'], ticks[1]['time'], times] 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["top"].set_visible(False)
self.__axes[index].spines["right"].set_visible(False) self.__axes[index].spines["right"].set_visible(False)
# Plot # Plot. There may be more than one set of data or each chart. Convert single data to list, then loop
if types[index] == 'plot': xdata_list = xdata[index] if isinstance(xdata[index], list) else [xdata[index], ]
self.__axes[index].plot(xdata[index], ydata[index]) ydata_list = ydata[index] if isinstance(ydata[index], list) else [ydata[index], ]
elif types[index] == 'scatter':
self.__axes[index].scatter(xdata[index], ydata[index], s=1) 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 # Layout with padding between charts
self.__fig.tight_layout(pad=0.5) self.__fig.tight_layout(pad=0.5)
+13 -9
View File
@@ -198,11 +198,11 @@ class TestCorrelation(unittest.TestCase):
# Patch it in # Patch it in
mock.copy_ticks_range.side_effect = [tick_data_s1, tick_data_s3, tick_data_s4] 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 # Start the monitor. Run every second. Use ~10 and ~5 seconds of data. Were not testing the overlap and price
# quality metrics here as that is set elsewhere so these can be set to not take effect. Set cache level high # data quality metrics here as that is set elsewhere so these can be set to not take effect. Set cache level
# and don't use autosave. Timer runs in a separate thread so test can continue after it has started. # 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, cor.start_monitor(interval=1, calculate_from=[0.66, 0.33], min_prices=0, max_set_size_diff_pct=0,
max_p_value=1, cache_time=100, autosave=False) overlap_pct=0, max_p_value=1, cache_time=100, autosave=False)
# Wait 2 seconds so timer runs twice # Wait 2 seconds so timer runs twice
time.sleep(2) time.sleep(2)
@@ -210,8 +210,12 @@ class TestCorrelation(unittest.TestCase):
# Stop the monitor # Stop the monitor
cor.stop_monitor() cor.stop_monitor()
# We should have 2 coefficients calculated for each symbol pair # 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), 6) 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') @patch('mt5_correlation.mt5.MetaTrader5')
def test_load_and_save(self, mock): 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 # Start monitor and run for a seconds with a 1 second interval to produce some coefficient history. Then stop
# the monitor # the monitor
cor.start_monitor(interval=1, from_mins=0.66, min_prices=0, max_set_size_diff_pct=0, overlap_pct=0, cor.start_monitor(interval=1, calculate_from=0.66, min_prices=0, max_set_size_diff_pct=0,
max_p_value=1, cache_time=100, autosave=False) overlap_pct=0, max_p_value=1, cache_time=100, autosave=False)
time.sleep(2) time.sleep(2)
cor.stop_monitor() cor.stop_monitor()