Added support for multiple timeframes for coefficient calculation.
This commit is contained in:
+7
-4
@@ -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
|
||||
|
||||
+111
-75
@@ -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)
|
||||
|
||||
+46
-25
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user