333 lines
17 KiB
Python
333 lines
17 KiB
Python
import math
|
|
import logging
|
|
import pandas as pd
|
|
from datetime import datetime
|
|
import time
|
|
import sched
|
|
import threading
|
|
import pytz
|
|
from scipy.stats.stats import pearsonr
|
|
|
|
from mt5_correlation.mt5 import MT5
|
|
|
|
|
|
class Correlation:
|
|
"""
|
|
A class to maintain the state of the calculated correlation coefficients.
|
|
"""
|
|
|
|
min_coefficient = 0.9
|
|
monitoring = False # Monitoring cor correlations
|
|
|
|
def __init__(self):
|
|
self.log = logging.getLogger(__name__)
|
|
|
|
# Create dataframe
|
|
columns = ['Symbol 1', 'Symbol 2', 'Base Coefficient', 'UTC Date From', 'UTC Date To', 'Timeframe',
|
|
'Last Check', 'Last Coefficient']
|
|
self.coefficient_data = pd.DataFrame(columns=columns)
|
|
|
|
# Create timer for continuous monitoring
|
|
self.scheduler = sched.scheduler(time.time, time.sleep)
|
|
|
|
def load(self, filename):
|
|
"""
|
|
Loads a csv file containing calculated coefficients
|
|
:param filename:
|
|
:return:
|
|
"""
|
|
self.coefficient_data = pd.read_csv(filename)
|
|
|
|
def save(self, filename):
|
|
"""
|
|
Saves the calculated coefficients as a csv file
|
|
:param filename:
|
|
:return:
|
|
"""
|
|
self.coefficient_data.to_csv(filename, index=False)
|
|
|
|
def calculate(self, date_from, date_to, timeframe, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90,
|
|
max_p_value=0.05):
|
|
"""
|
|
Calculates correlation coefficient between all symbols in MetaTrader5 Market Watch. Updates coefficient data.
|
|
|
|
:param date_from: From date for price data from which to calculate correlation coefficients
|
|
:param date_to: To date for price data from which to calculate correlation coefficients
|
|
:param timeframe: Timeframe for price data from which to calculate correlation coefficients
|
|
: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
|
|
within this pct of each other
|
|
:param overlap_pct:
|
|
:param max_p_value: The maximum p value for the correlation to be meaningful
|
|
|
|
:return:
|
|
"""
|
|
|
|
# Create mt5 class. This contains required methods for interacting with MT5.
|
|
mt5 = MT5()
|
|
|
|
# Gte all visible symbols
|
|
symbols = mt5.get_symbols()
|
|
|
|
# Get price data for selected symbols. 1 week of 15 min OHLC data for each symbol. Add to dict.
|
|
price_data = {}
|
|
for symbol in symbols:
|
|
price_data[symbol] = mt5.get_prices(symbol=symbol, from_date=date_from, to_date=date_to,
|
|
timeframe=timeframe)
|
|
|
|
# Loop through all symbol pair combinations and calculate coefficient. Make sure you don't double count pairs
|
|
# eg. (USD/GBP AUD/USD vs AUD/USD USD/GBP). Use grid of all symbols with i and j axis. j starts at i + 1 to
|
|
# avoid duplicating. We will store all coefficients in a dataframe for export as CSV.
|
|
index = 0
|
|
# There will be (x^2 - x) / 2 pairs where x is number of symbols
|
|
num_pair_combinations = int((len(symbols) ** 2 - len(symbols)) / 2)
|
|
|
|
for i in range(0, len(symbols)):
|
|
symbol1 = symbols[i]
|
|
|
|
for j in range(i + 1, len(symbols)):
|
|
symbol2 = symbols[j]
|
|
index += 1
|
|
|
|
# Get price data for both symbols
|
|
symbol1_price_data = price_data[symbol1]
|
|
symbol2_price_data = price_data[symbol2]
|
|
|
|
# Get coefficient and store if valid
|
|
coefficient = self.calculate_coefficient(symbol1_prices=symbol1_price_data,
|
|
symbol2_prices=symbol2_price_data,
|
|
min_prices=min_prices,
|
|
max_set_size_diff_pct=max_set_size_diff_pct,
|
|
overlap_pct=overlap_pct, max_p_value=max_p_value)
|
|
|
|
if coefficient is not None:
|
|
|
|
self.coefficient_data = \
|
|
self.coefficient_data.append({'Symbol 1': symbol1, 'Symbol 2': symbol2,
|
|
'Base Coefficient': coefficient, 'UTC Date From': date_from,
|
|
'UTC Date To': date_to, 'Timeframe': timeframe},
|
|
ignore_index=True)
|
|
self.log.debug(f"Pair {index} of {num_pair_combinations}: {symbol1}:{symbol2} has a "
|
|
f"coefficient of {coefficient}.")
|
|
else:
|
|
self.log.debug(f"Coefficient for pair {index} of {num_pair_combinations}: {symbol1}:"
|
|
f"{symbol2} could no be calculated.")
|
|
|
|
# Sort, highest correlated first
|
|
self.coefficient_data = self.coefficient_data.sort_values('Base Coefficient', ascending=False)
|
|
|
|
def update_coefficient(self, symbol1, symbol2, date_from, date_to, min_prices=100, max_set_size_diff_pct=90,
|
|
overlap_pct=90, max_p_value=0.05):
|
|
"""
|
|
Updates the coefficient for the specified symbol pair
|
|
:param symbol1: Name of symbol to calculate coefficient for.
|
|
:param symbol2: Name of symbol to calculate coefficient for.
|
|
:param date_from: From date for tick data from which to calculate correlation coefficients
|
|
:param date_to: To date for tick data from which to calculate correlation coefficients
|
|
: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
|
|
within this pct of each other
|
|
:param overlap_pct:
|
|
:param max_p_value: The maximum p value for the correlation to be meaningful
|
|
:return: correlation coefficient, or None if coefficient could not be calculated.
|
|
"""
|
|
|
|
# Get the tick data
|
|
mt5 = MT5()
|
|
symbol1ticks = mt5.get_ticks(symbol=symbol1, from_date=date_from, to_date=date_to)
|
|
symbol2ticks = mt5.get_ticks(symbol=symbol2, from_date=date_from, to_date=date_to)
|
|
|
|
# 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.set_index('time', inplace=True)
|
|
symbol2ticks.set_index('time', inplace=True)
|
|
symbol1prices = symbol1ticks['ask'].resample('1S').ohlc()
|
|
symbol2prices = symbol2ticks['ask'].resample('1S').ohlc()
|
|
symbol1prices.reset_index(inplace=True)
|
|
symbol2prices.reset_index(inplace=True)
|
|
symbol1prices = symbol1prices[symbol1prices['close'].notna()]
|
|
symbol2prices = symbol2prices[symbol2prices['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)
|
|
else:
|
|
coefficient = None
|
|
|
|
# Find the correct row in the coefficient data and update with calculation date and calculated coefficient
|
|
timezone = pytz.timezone("Etc/UTC")
|
|
now = datetime.now(tz=timezone)
|
|
|
|
# Update data if we have a coefficient
|
|
if coefficient is not None:
|
|
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
|
|
|
|
return coefficient
|
|
|
|
def update_all_coefficients(self, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90,
|
|
max_p_value=0.05):
|
|
"""
|
|
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 date_from: From date for tick data from which to calculate correlation coefficients
|
|
:param date_to: To date for tick data from which to calculate correlation coefficients
|
|
: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
|
|
within this pct of each other
|
|
:param overlap_pct:
|
|
:param max_p_value: The maximum p value for the correlation to be meaningful
|
|
:return: correlation coefficient, or None if coefficient could not be calculated.
|
|
"""
|
|
# Update latest coefficient for every pair
|
|
for index, row in self.filtered_coefficient_data.iterrows():
|
|
symbol1 = row['Symbol 1']
|
|
symbol2 = row['Symbol 2']
|
|
self.update_coefficient(symbol1=symbol1, symbol2=symbol2, 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)
|
|
|
|
def start_monitor(self, interval, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90,
|
|
max_p_value=0.05):
|
|
"""
|
|
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 date_from: From date for tick data from which to calculate correlation coefficients
|
|
:param date_to: To date for tick data from which to calculate correlation coefficients
|
|
: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
|
|
within this pct of each other
|
|
:param overlap_pct:
|
|
:param max_p_value: The maximum p value for the correlation to be meaningful
|
|
:return: correlation coefficient, or None if coefficient could not be calculated.
|
|
|
|
:return:
|
|
"""
|
|
self.log.debug(f"Starting monitor.")
|
|
self.monitoring = True
|
|
|
|
# Create thread to run monitoring This will call private __monitor method that will run the calculation and
|
|
# keep scheduling itself while self.monitoring is True
|
|
params = {'interval': interval, '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}
|
|
thread = threading.Thread(target=self.__monitor, kwargs=params)
|
|
thread.start()
|
|
|
|
def stop_monitor(self):
|
|
"""
|
|
Stops monitoring symbol pairs for correlation.
|
|
:return:
|
|
"""
|
|
self.log.debug(f"Stopping monitor.")
|
|
self.monitoring = False
|
|
|
|
def __monitor(self, interval, date_from, date_to, min_prices=100, max_set_size_diff_pct=90, overlap_pct=90,
|
|
max_p_value=0.05):
|
|
"""
|
|
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 date_from: From date for tick data from which to calculate correlation coefficients
|
|
:param date_to: To date for tick data from which to calculate correlation coefficients
|
|
: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
|
|
within this pct of each other
|
|
:param overlap_pct:
|
|
:param max_p_value: The maximum p value for the correlation to be meaningful
|
|
:return: correlation coefficient, or None if coefficient could not be calculated.
|
|
:return:
|
|
"""
|
|
self.log.debug(f"In monitor event. Monitoring: {self.monitoring}.")
|
|
|
|
# Only run if monitor is not stopped
|
|
if self.monitoring:
|
|
# Update all coefficients
|
|
self.update_all_coefficients(date_from=date_from, date_to=date_to, min_prices=min_prices,
|
|
max_set_size_diff_pct=max_set_size_diff_pct, overlap_pct=overlap_pct,
|
|
max_p_value=max_p_value)
|
|
|
|
# Schedule the timer to run again
|
|
params = {'interval': interval, '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}
|
|
self.scheduler.enter(delay=interval, priority=1, action=self.__monitor, kwargs=params)
|
|
self.scheduler.run()
|
|
|
|
@property
|
|
def filtered_coefficient_data(self):
|
|
"""
|
|
:return: Coefficient data filtered so that all base coefficients >= min coefficient
|
|
"""
|
|
if self.coefficient_data is not None:
|
|
return self.coefficient_data.loc[self.coefficient_data['Base Coefficient'] >= self.min_coefficient]
|
|
else:
|
|
return None
|
|
|
|
@staticmethod
|
|
def calculate_coefficient(symbol1_prices, symbol2_prices, min_prices=100, max_set_size_diff_pct=90,
|
|
overlap_pct=90, max_p_value=0.05):
|
|
"""
|
|
Calculates the correlation coefficient between two sets of price data. Uses close price.
|
|
|
|
:param symbol1_prices: prices or ticks for symbol 1
|
|
:param symbol2_prices: prices or ticks for symbol 2
|
|
: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
|
|
within this pct of each other
|
|
:param overlap_pct:
|
|
:param max_p_value: The maximum p value for the correlation to be meaningful
|
|
:return: correlation coefficient, or None if coefficient could not be calculated.
|
|
"""
|
|
|
|
# Calculate size of intersection and determine if prices for symbols have enough overlapping timestamps for
|
|
# correlation coefficient calculation to be meaningful. Is the smallest set at least max_set_size_diff_pct % of
|
|
# the size of the largest set and is the overlap set size at least overlap_pct % the size of the smallest set?
|
|
coefficient = None
|
|
|
|
intersect_dates = (set(symbol1_prices['time']) & set(symbol2_prices['time']))
|
|
len_smallest_set = int(min([len(symbol1_prices.index), len(symbol2_prices.index)]))
|
|
len_largest_set = int(max([len(symbol1_prices.index), len(symbol2_prices.index)]))
|
|
similar_size = len_largest_set * (max_set_size_diff_pct / 100) <= len_smallest_set
|
|
enough_overlap = len(intersect_dates) >= len_smallest_set * (overlap_pct / 100)
|
|
enough_prices = len_smallest_set >= min_prices
|
|
suitable = similar_size and enough_overlap and enough_prices
|
|
|
|
if suitable:
|
|
# Calculate coefficient on close prices
|
|
|
|
# First filter prices to only include those that intersect
|
|
symbol1_prices_filtered = symbol1_prices[symbol1_prices['time'].isin(intersect_dates)]
|
|
symbol2_prices_filtered = symbol2_prices[symbol2_prices['time'].isin(intersect_dates)]
|
|
|
|
# Calculate coefficient. Only use if p value is < 0.01 (highly likely that coefficient is valid and null
|
|
# hypothesis is false).
|
|
coefficient_with_p_value = pearsonr(symbol1_prices_filtered['close'], symbol2_prices_filtered['close'])
|
|
coefficient = None if coefficient_with_p_value[1] >= max_p_value else coefficient_with_p_value[0]
|
|
|
|
# If NaN, change to None
|
|
if coefficient is not None and math.isnan(coefficient):
|
|
coefficient = None
|
|
|
|
return coefficient
|