Added charts

This commit is contained in:
Jamie Cash
2021-02-26 17:55:39 +00:00
parent e44f9451d0
commit 7ca46b1be2
4 changed files with 462 additions and 162 deletions
+6 -5
View File
@@ -3,20 +3,21 @@ calculate:
from:
days: 10
timeframe: 15
min_prices: 500
min_prices: 600
max_set_size_diff_pct: 90
overlap_pct: 90
max_p_value: 0.05
monitor:
from:
minutes: 60
minutes: 10
interval: 10
min_prices: 500
max_set_size_diff_pct: 90
overlap_pct: 90
min_prices: 300
max_set_size_diff_pct: 50
overlap_pct: 50
max_p_value: 0.05
monitoring_threshold: 0.9
divergence_threshold: 0.8
tick_cache_time: 10
logging:
version: 1
disable_existing_loggers: false
+242 -96
View File
@@ -1,12 +1,14 @@
import math
import logging
import pandas as pd
from datetime import datetime
from datetime import datetime, timedelta
import time
import sched
import threading
import pytz
from scipy.stats.stats import pearsonr
import yaml
import pickle
from mt5_correlation.mt5 import MT5
@@ -16,6 +18,9 @@ class Correlation:
A class to maintain the state of the calculated correlation coefficients.
"""
# Connection to metatrader
__mt5 = None
# Minimum base coefficient for monitoring. Symbol pairs with a lower correlation
# coefficient than ths won't be monitored.
monitoring_threshold = 0.9
@@ -24,13 +29,25 @@ class Correlation:
__monitoring = False
__monitoring_params = {}
# The price data used to calculate the correlations
__price_data = None
# Coefficient data and history. Will be created as dataframes in Init
coefficient_data = None
coefficient_history = None
# Cache for ticks. Dict: {Symbol: [retrieved datetime, ticks dataframe]}
__ticks = {}
def __init__(self):
# Logger
self.__log = logging.getLogger(__name__)
# Create dataframe
self.__columns = ['Symbol 1', 'Symbol 2', 'Base Coefficient', 'UTC Date From', 'UTC Date To', 'Timeframe',
'Last Check', 'Last Coefficient']
self.coefficient_data = pd.DataFrame(columns=self.__columns)
# Connection to metatrader
self.__mt5 = MT5()
# Create dataframe for coefficient data
self.__reset_coefficient_data()
# Create timer for continuous monitoring
self.__scheduler = sched.scheduler(time.time, time.sleep)
@@ -45,22 +62,38 @@ class Correlation:
else:
return None
def load(self, filename):
def load(self, filename, price_data_filename=None):
"""
Loads a csv file containing calculated coefficients
:param filename:
Loads a csv file containing calculated coefficients, and optionally the price data used to calculate those
coefficients
:param filename: The filename for the coefficient data to load.
:param price_data_filename: The filename for the price data to load.
:return:
"""
# Load coefficients file
self.coefficient_data = pd.read_csv(filename)
def save(self, filename):
# If specified, load price data yaml file
if price_data_filename is not None:
self.__price_data = {} # Clear
with open(price_data_filename, 'rb') as file:
self.__price_data = pickle.load(file)
def save(self, filename, price_data_filename=None):
"""
Saves the calculated coefficients as a csv file
:param filename:
:param filename: The filename for the coefficient data to save to.
:param price_data_filename: The filename for the price data to save to.
:return:
"""
# Save the coefficient data
self.coefficient_data.to_csv(filename, index=False)
# Save the price data if required
if price_data_filename is not None:
with open(price_data_filename, 'wb') as file:
pickle.dump(self.__price_data, file, protocol=pickle.HIGHEST_PROTOCOL)
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):
"""
@@ -79,25 +112,24 @@ class Correlation:
:return:
"""
coefficient = None
# If we are monitoring, stop. We will need to restart later
was_monitoring = self.__monitoring
if self.__monitoring:
self.stop_monitor()
# Clear the existing correlations
self.coefficient_data = pd.DataFrame(columns=self.__columns)
self.__reset_coefficient_data()
# Create mt5 class. This contains required methods for interacting with MT5.
mt5 = MT5()
# Gte all visible symbols
symbols = mt5.get_symbols()
# Get all visible symbols
symbols = self.__mt5.get_symbols()
# Get price data for selected symbols. 1 week of 15 min OHLC data for each symbol. Add to dict.
price_data = {}
self.__price_data = {}
for symbol in symbols:
price_data[symbol] = mt5.get_prices(symbol=symbol, from_date=date_from, to_date=date_to,
timeframe=timeframe)
self.__price_data[symbol] = self.__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
@@ -114,16 +146,18 @@ class Correlation:
index += 1
# Get price data for both symbols
symbol1_price_data = price_data[symbol1]
symbol2_price_data = price_data[symbol2]
symbol1_price_data = self.__price_data[symbol1]
symbol2_price_data = self.__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)
# Get coefficient
if symbol1_price_data is not None and symbol2_price_data is not None:
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)
# Store if valid
if coefficient is not None:
self.coefficient_data = \
@@ -143,28 +177,40 @@ class Correlation:
# If we were monitoring, we stopped, so start again.
if was_monitoring:
self.start_monitor(interval=self.__monitoring_params['interval'],
date_from=self.__monitoring_params['date_from'],
date_to=self.__monitoring_params['date_to'],
from_mins=self.__monitoring_params['from_mins'],
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'],
max_p_value=self.__monitoring_params['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):
def get_price_data(self, symbol):
"""
Returns the price data used to calculate the base coefficients for the specified symbol
:param symbol: Symbol to get price data for.
:return: price data
"""
price_data = None
if symbol in self.__price_data:
price_data = self.__price_data[symbol]
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):
"""
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 from_mins: The number of minutes of tick data to use for calculations
: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
:param cache_time: Tick data is cached so that we can check coefficients for multiple symbol pairs and reuse
the tick data. Number of seconds to cache tick data for before it becomes stale.
:return: correlation coefficient, or None if coefficient could not be calculated.
"""
@@ -180,9 +226,9 @@ class Correlation:
# 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, 'date_from': date_from, 'date_to': date_to,
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}
'overlap_pct': overlap_pct, 'max_p_value': max_p_value, 'cache_time': cache_time}
thread = threading.Thread(target=self.__monitor, kwargs=self.__monitoring_params)
thread.start()
@@ -197,23 +243,27 @@ class Correlation:
else:
self.__log.debug(f"Request to stop monitor when it is not running. No action taken.")
@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):
def calculate_coefficient(self, symbol1_prices, symbol2_prices, min_prices: int = 100,
max_set_size_diff_pct: int = 90, overlap_pct: int = 90,
max_p_value: float = 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 symbol1_prices: Pandas dataframe containing prices or ticks for symbol 1
:param symbol2_prices: Pandas dataframe containing 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.
:rtype: float or None
"""
assert symbol1_prices is not None and symbol2_prices is not None
# 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?
@@ -237,29 +287,47 @@ class Correlation:
# 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]
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
self.__log.debug(f"Calculate coefficient returning {coefficient}. "
f"Symbol 1 Prices: {len(symbol1_prices)} Symbol 2 Prices: {len(symbol2_prices)} "
f"Overlap Prices: {len(intersect_dates)} Similar size: {similar_size} "
f"Enough overlap: {enough_overlap} Enough prices: {enough_prices} Suitable: {suitable}.")
return coefficient
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):
def get_coefficient_history(self, symbol1, symbol2):
"""
Returns the coefficient history for the specified symbol pair calculated during this instance.
Coefficient history does not persist between instances.
:param symbol1:
:param symbol2:
:return: dataframe containing history of coefficient data.
"""
history = self.coefficient_history[(self.coefficient_history['Symbol 1'] == symbol1) &
(self.coefficient_history['Symbol 2'] == symbol2)]
return history
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):
"""
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 from_mins: The number of minutes of tick data to use for calculations
: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
:param cache_time: Tick data is cached so that we can check coefficients for multiple symbol pairs and reuse
the tick data. Number of seconds to cache tick data for before it becomes stale.
:return: correlation coefficient, or None if coefficient could not be calculated.
"""
@@ -268,66 +336,166 @@ class Correlation:
# 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,
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)
max_p_value=max_p_value, cache_time=cache_time)
# Schedule the timer to run again
params = {'interval': interval, 'date_from': date_from, 'date_to': date_to, 'min_prices': min_prices,
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}
'max_p_value': max_p_value, "cache_time": cache_time}
self.__scheduler.enter(delay=interval, priority=1, action=self.__monitor, kwargs=params)
self.__scheduler.run()
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):
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):
"""
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 from_mins: The number of minutes of tick data to use for calculations
: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
:param cache_time: Tick data is cached so that we can check coefficients for multiple symbol pairs and reuse
the tick data. Number of seconds to cache tick data for before it becomes stale.
:return: correlation coefficient, or None if coefficient could not be calculated.
"""
coefficient = None
# Get dates
# From and to dates for calculations.
timezone = pytz.timezone("Etc/UTC")
date_to = datetime.now(tz=timezone)
date_from = date_to - timedelta(minutes=from_mins)
# 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)
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.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()]
symbol1ticks = symbol1ticks.set_index('time')
symbol2ticks = symbol2ticks.set_index('time')
try:
symbol1prices = symbol1ticks['ask'].resample('1S').ohlc()
symbol2prices = symbol2ticks['ask'].resample('1S').ohlc()
except RecursionError:
self.__log.warning(f"Coefficient could not be calculated for {symbol1}:{symbol2} as 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()]
# 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 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)
self.__log.debug(f"Symbol pair {symbol1}:{symbol2} has a coefficient of {coefficient}.")
else:
coefficient = None
# Find the correct row in the coefficient data and update with calculation date and calculated coefficient
# 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)
return coefficient
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):
"""
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 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
:param cache_time: Tick data is cached so that we can check coefficients for multiple symbol pairs and reuse
the tick data. Number of seconds to cache tick data for before it becomes stale.
: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, 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)
def __get_ticks(self, symbol, date_from, date_to, cache_time):
"""
Returns the ticks for the specified symbol. Get's from cache if available and not older than cache_timeframe.
:param symbol: Name of symbol to get ticks for.
:param date_from:
:param date_to:
:param cache_time: Number of seconds before cached data is stale. If > than this number of seconds has elapsed,
get data from source and refresh cache.
:return:
"""
timezone = pytz.timezone("Etc/UTC")
utc_now = datetime.now(tz=timezone)
# Check if in cache and not stale
if symbol in self.__ticks and utc_now < self.__ticks[symbol][0] + timedelta(seconds=cache_time):
# Cached ticks are not stale. Get them
ticks = self.__ticks[symbol][1]
self.__log.debug(f"Ticks for {symbol} retrieved from cache.")
else:
# Data does not exist in cache or cached data is stale. Retrieve from source and cache.
ticks = self.__mt5.get_ticks(symbol=symbol, from_date=date_from, to_date=date_to)
self.__ticks[symbol] = [utc_now, ticks]
self.__log.debug(f"Ticks for {symbol} retrieved from source and cached.")
return ticks
def __reset_coefficient_data(self):
"""
Clears coefficient data and history.
:return:
"""
# Create dataframe 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']
self.coefficient_history = pd.DataFrame(columns=coefficient_history_columns)
def __update_coefficient_data(self, symbol1, symbol2, coefficient, 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:
:return:
"""
timezone = pytz.timezone("Etc/UTC")
now = datetime.now(tz=timezone)
# Update data if we have a coefficient
# 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),
@@ -337,28 +505,6 @@ class Correlation:
(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)
row = pd.DataFrame(columns=self.coefficient_history.columns,
data=[[symbol1, symbol2, coefficient, date_from, date_to]])
self.coefficient_history = self.coefficient_history.append(row)
+193 -47
View File
@@ -1,5 +1,10 @@
import wx
import wx.grid
import matplotlib.pyplot as plt
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
import matplotlib.dates
import matplotlib
from mt5_correlation.correlation import Correlation
from mt5_correlation.config import Config, SettingsDialog
from datetime import datetime, timedelta
@@ -7,15 +12,21 @@ import pytz
import pandas as pd
import logging
import logging.config
import os
matplotlib.use('WXAgg')
class MonitorFrame(wx.Frame):
cor = None
rows = 0 # Need to track as we need to notify grid if row count changes.
opened_filename = None # So we can save to same file as we opened
__opened_filename = None # So we can save to same file as we opened
config = None # The applications config
__selected_correlation = [] # List of Symbol 1 & Symbol 2
# Columns for coefficient table
COLUMN_INDEX = 0
COLUMN_SYMBOL1 = 1
COLUMN_SYMBOL2 = 2
@@ -31,7 +42,7 @@ class MonitorFrame(wx.Frame):
wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title="Divergence Monitor")
# Create logger and get config
self.log = logging.getLogger(__name__)
self.__log = logging.getLogger(__name__)
self.config = Config()
# Create correlation instance to maintain state of calculated coefficients. Set min coefficient from config
@@ -76,8 +87,8 @@ class MonitorFrame(wx.Frame):
panel = wx.Panel(self, wx.ID_ANY)
toggle_sizer = wx.BoxSizer(wx.HORIZONTAL) # Label and toggle
correlations_sizer = wx.BoxSizer(wx.VERTICAL) # Toggle sizer and correlations grid
main_sizer = wx.BoxSizer(wx.HORIZONTAL) # Correlations sizer and graphs panel
panel.SetSizer(main_sizer)
self.__main_sizer = wx.BoxSizer(wx.HORIZONTAL) # Correlations sizer and graphs panel
panel.SetSizer(self.__main_sizer)
# Create the label and toggle, populate the toggle sizer and add the toggle sizer to the correlations sizer
monitor_toggle_label = wx.StaticText(panel, id=wx.ID_ANY, label="Monitoring")
@@ -111,12 +122,13 @@ class MonitorFrame(wx.Frame):
self.grid_correlations.SetMaxSize((520, -1))
correlations_sizer.Add(self.grid_correlations, 1, wx.ALL | wx.EXPAND, 1)
# Create the charts
charts = wx.StaticText(panel, wx.ID_ANY, "Charts Go Here", style=wx.ALIGN_CENTER_HORIZONTAL)
# Create the charts and hide as we have no data to display yet
self.__graph = GraphPanel(panel)
self.__graph.Hide()
# Add the correlations sizer and the charts to the main sizer.
main_sizer.Add(correlations_sizer, 1, wx.ALL | wx.EXPAND, 1)
main_sizer.Add(charts, 1, wx.ALL | wx.EXPAND, 1)
self.__main_sizer.Add(correlations_sizer, 0, wx.ALL | wx.EXPAND, 1)
self.__main_sizer.Add(self.__graph, 1, wx.ALL | wx.EXPAND, 1)
# Size the window.
self.SetSize((800, 500))
@@ -129,7 +141,7 @@ class MonitorFrame(wx.Frame):
self.monitor_toggle.Bind(wx.EVT_TOGGLEBUTTON, self.monitor)
# Bind timer
self.Bind(wx.EVT_TIMER, self.refresh_grid, self.timer)
self.Bind(wx.EVT_TIMER, self.__timer_event, self.timer)
# Bind menu items
self.Bind(wx.EVT_MENU, self.open_file, menu_item_open)
@@ -139,6 +151,9 @@ class MonitorFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.open_settings, menu_item_settings)
self.Bind(wx.EVT_MENU, self.quit, menu_item_exit)
# Bind row select
self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.select_cell, self.grid_correlations)
# Bind window close event
self.Bind(wx.EVT_CLOSE, self.on_close, self)
@@ -149,18 +164,28 @@ class MonitorFrame(wx.Frame):
if fileDialog.ShowModal() == wx.ID_CANCEL:
return # the user changed their mind
# Load the file chosen by the user
self.opened_filename = fileDialog.GetPath()
self.cor.load(self.opened_filename)
# Load the file chosen by the user. Also load the corresponding data file if there is one.
self.__opened_filename = fileDialog.GetPath()
data_filename = f"{os.path.splitext(self.__opened_filename)[0]}.price.data"
if os.path.isfile(data_filename):
self.cor.load(self.__opened_filename, price_data_filename=data_filename)
else:
self.cor.load(self.__opened_filename)
# Refresh data in grid
self.refresh_grid(event)
self.refresh_grid()
self.SetStatusText(f"File {self.opened_filename} loaded.")
self.SetStatusText(f"File {self.__opened_filename} loaded.")
def save_file(self, event):
self.cor.save(self.opened_filename)
self.SetStatusText(f"File saved as {self.opened_filename}")
self.SetStatusText(f"Saving file as {self.__opened_filename}")
if self.__opened_filename is None:
self.save_file_as(event)
else:
self.cor.save(self.__opened_filename)
self.SetStatusText(f"File saved as {self.__opened_filename}")
def save_file_as(self, event):
with wx.FileDialog(self, "Save Coefficients file", wildcard="CSV (*.csv)|*.csv",
@@ -168,11 +193,14 @@ class MonitorFrame(wx.Frame):
if fileDialog.ShowModal() == wx.ID_CANCEL:
return # the user changed their mind
# Save the file, changing opened filename so next save writes to new file
self.opened_filename = fileDialog.GetPath()
self.cor.save(self.opened_filename)
# Save the file and price data file, changing opened filename so next save writes to new file
self.SetStatusText(f"Saving file as {self.__opened_filename}")
self.SetStatusText(f"File saved as {self.opened_filename}")
self.__opened_filename = fileDialog.GetPath()
data_filename = f"{os.path.splitext(self.__opened_filename)[0]}.price.data"
self.cor.save(self.__opened_filename, price_data_filename=data_filename)
self.SetStatusText(f"File saved as {self.__opened_filename}")
def calculate_coefficients(self, event):
# set time zone to UTC to avoid local offset issues, and get from and to dates (a week ago to today)
@@ -191,30 +219,30 @@ class MonitorFrame(wx.Frame):
self.SetStatusText("")
# Show calculated data
self.refresh_grid(event)
self.refresh_grid()
def quit(self, event):
self.Close()
def refresh_grid(self, event):
def refresh_grid(self):
"""
Refreshes grid. Notifies if rows have been added or deleted.
:return:
"""
self.log.debug(f"Refreshing grid. Timer running: {self.timer.IsRunning()}")
self.__log.debug(f"Refreshing grid. Timer running: {self.timer.IsRunning()}")
# Update data
self.table.data = self.cor.coefficient_data.copy()
# Format
self.table.data['Base Coefficient'] = self.table.data['Base Coefficient'].map('{:.5f}'.format)
self.table.data['Last Check'] = pd.to_datetime(self.table.data['Last Check'], utc=True)
self.table.data['Last Check'] = self.table.data['Last Check'].dt.strftime('%d-%m-%y %H:%M:%S')
self.table.data['Last Coefficient'] = self.table.data['Last Coefficient'].map('{:.5f}'.format)
self.table.data.loc[:, 'Base Coefficient'] = self.table.data['Base Coefficient'].map('{:.5f}'.format)
self.table.data.loc[:, 'Last Check'] = pd.to_datetime(self.table.data['Last Check'], utc=True)
self.table.data.loc[:, 'Last Check'] = self.table.data['Last Check'].dt.strftime('%d-%m-%y %H:%M:%S')
self.table.data.loc[:, 'Last Coefficient'] = self.table.data['Last Coefficient'].map('{:.5f}'.format)
# Remove nans. The ones from the float column wil be str nan as they have been formatted
# Remove nans. The ones from the float column will be str nan as they have been formatted
self.table.data = self.table.data.fillna('')
self.table.data['Last Coefficient'] = self.table.data['Last Coefficient'].replace('nan', '')
self.table.data.loc[:, 'Last Coefficient'] = self.table.data['Last Coefficient'].replace('nan', '')
# Start refresh
self.grid_correlations.BeginBatch()
@@ -244,24 +272,21 @@ class MonitorFrame(wx.Frame):
def monitor(self, event):
# Check state of toggle button. If on, then start monitoring, else stop
if self.monitor_toggle.GetValue():
self.log.debug("Starting monitoring.")
self.__log.info("Starting monitoring.")
self.monitor_toggle.SetBackgroundColour(wx.GREEN)
self.monitor_toggle.SetLabelText("On")
self.SetStatusText("Monitoring for changes to coefficients.")
# From and to dates for calculations.
timezone = pytz.timezone("Etc/UTC")
utc_to = datetime.now(tz=timezone)
utc_from = utc_to - timedelta(minutes=self.config.get('monitor.from.minutes'))
self.timer.Start(self.config.get('monitor.interval')*1000)
self.cor.start_monitor(interval=self.config.get('monitor.interval'), date_from=utc_from, date_to=utc_to,
self.cor.start_monitor(interval=self.config.get('monitor.interval'),
from_mins=self.config.get('monitor.from.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'),
max_p_value=self.config.get('monitor.max_p_value'))
max_p_value=self.config.get('monitor.max_p_value'),
cache_time=self.config.get('monitor.tick_cache_time'))
else:
self.log.debug("Stopping monitoring.")
self.__log.info("Stopping monitoring.")
self.monitor_toggle.SetBackgroundColour(wx.RED)
self.monitor_toggle.SetLabelText("Off")
self.SetStatusText("Monitoring stopped.")
@@ -288,7 +313,7 @@ class MonitorFrame(wx.Frame):
# If 'monitor.interval has changed then restart gui timer.
# If 'monitor.monitoring_threshold' has changed, then refresh correlation data.
# If any 'logging.' settings have changed, then reload logger config.
if setting.startswith('monitor. ') and setting != 'monitor.divergence_threshold':
if setting.startswith('monitor.') and setting != 'monitor.divergence_threshold':
restart_monitor_timer = True
if setting == 'monitor.interval':
restart_gui_timer = True
@@ -299,29 +324,30 @@ class MonitorFrame(wx.Frame):
# Now perform the actions
if restart_monitor_timer:
self.log.debug("Settings updated. Reloading monitoring timer.")
self.__log.info("Settings updated. Reloading monitoring timer.")
self.cor.stop_monitor()
# From and to dates for calculations.
timezone = pytz.timezone("Etc/UTC")
utc_to = datetime.now(tz=timezone)
utc_from = utc_to - timedelta(minutes=self.config.get('monitor.from.minutes'))
self.cor.start_monitor(interval=self.config.get('monitor.interval'), date_from=utc_from, date_to=utc_to,
self.cor.start_monitor(interval=self.config.get('monitor.interval'),
from_mins=self.config.get('monitor.from.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'),
max_p_value=self.config.get('monitor.max_p_value'))
if restart_gui_timer:
self.log.debug("Settings updated. Restarting gui timer.")
self.__log.info("Settings updated. Restarting gui timer.")
self.timer.Stop()
self.timer.Start(self.config.get('monitor.interval') * 1000)
if reload_correlations:
self.log.debug("Settings updated. Updating monitoring threshold and reloading grid.")
self.__log.info("Settings updated. Updating monitoring threshold and reloading grid.")
self.cor.monitoring_threshold = self.config.get("monitor.monitoring_threshold")
self.refresh_grid(event)
self.refresh_grid()
if reload_logger:
self.log.debug("Settings updated. Reloading logger.")
self.__log.info("Settings updated. Reloading logger.")
log_config = Config().get('logging')
logging.config.dictConfig(log_config)
@@ -331,13 +357,64 @@ class MonitorFrame(wx.Frame):
:param event:
:return:
"""
if self.opened_filename is not None:
self.cor.save(self.opened_filename)
if self.__opened_filename is not None:
self.cor.save(self.__opened_filename)
self.cor.stop_monitor()
# Kill graph as it seems to be stopping script from ending
self.__graph = None
# End
event.Skip()
def select_cell(self, event):
"""
A cell was selected. Show the graph for the correlation.
:param event:
:return:
"""
# Get row and symbols.
row = event.GetRow()
symbol1 = self.grid_correlations.GetCellValue(row, self.COLUMN_SYMBOL1)
symbol2 = self.grid_correlations.GetCellValue(row, self.COLUMN_SYMBOL2)
self.__selected_correlation = [symbol1, symbol2]
self.show_graph(symbol1, symbol2)
def show_graph(self, symbol1, symbol2):
"""
Displays the graph for the specified symbols correlation history
:param symbol1:
:param symbol2:
:return:
"""
# Get the data price data for the base coefficient calculation and the coefficient history data
symbol_1_price_data = self.cor.get_price_data(symbol1)
symbol_2_price_data = self.cor.get_price_data(symbol2)
history_data = self.cor.get_coefficient_history(symbol1, symbol2)
times = history_data['UTC Date To']
coefficients = history_data['Coefficient']
# 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],
symbols=[symbol1, symbol2])
# Un-hide and layout if hidden
if not self.__graph.IsShown():
self.__graph.Show()
self.__main_sizer.Layout()
def __timer_event(self, event):
"""
Called on timer event. Refreshes grid and updatates selected graph.
:return:
"""
self.refresh_grid()
if len(self.__selected_correlation) == 2:
self.show_graph(symbol1=self.__selected_correlation[0], symbol2=self.__selected_correlation[1])
class DataTable(wx.grid.GridTableBase):
"""
@@ -393,3 +470,72 @@ class DataTable(wx.grid.GridTableBase):
attr.SetBackgroundColour(wx.WHITE)
return attr
class GraphPanel(wx.Panel):
def __init__(self, parent):
# Super
wx.Panel.__init__(self, parent)
# 3 axis, price data 1, price data 2 and coefficient data. All will have axis labels and top and right boarders
# removed
self.__fig, self.__axes = plt.subplots(nrows=3, ncols=1)
# Create the canvas
self.__canvas = FigureCanvas(self, -1, self.__fig)
# Date format for x axes
self.__tick_fmt_date = matplotlib.dates.DateFormatter('%d-%b')
self.__tick_fmt_time = matplotlib.dates.DateFormatter('%H:%M:%S')
# Sizer etc.
self.__sizer = wx.BoxSizer(wx.VERTICAL)
self.__sizer.Add(self.__canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
self.SetSizer(self.__sizer)
self.Fit()
def __del__(self):
# Close all plots
plt.close('all')
self.__axes = None
self.__fig = None
def draw(self, times, coefficients, prices=None, symbols=None):
"""
Plot the correlations.
:param times: Series of time values for x axis
:param coefficients: Series of coefficients values for y axis
:param prices: Price data used to calculate base coefficient. List [Symbol1 Price Data, Symbol 2 Price Data]
:param symbols: Symbols. List [Symbol1, Symbol2]
:return:
"""
# Clear. We will need to redraw
for ax in self.__axes:
ax.clear()
if symbols is not None and len(symbols) == 2:
# Price history chart for both symbols
for i in range(0, 2):
if symbols[i] is not None and prices is not None and len(prices) == 2 and prices[i] is not None:
self.__axes[i].set_title(f"Base Coefficient Price Data for {symbols[i]}")
self.__axes[i].set_xlabel('Time')
self.__axes[i].set_ylabel('Price')
self.__axes[i].plot(prices[i]['time'], prices[i]['close'])
self.__axes[i].xaxis.set_major_formatter(self.__tick_fmt_date)
self.__axes[i].xaxis.set_minor_formatter(self.__tick_fmt_time)
plt.setp(self.__axes[i].xaxis.get_majorticklabels(), rotation=45)
# Coefficient history chart
self.__axes[2].set_title(f"Coefficient History for {symbols[0]}:{symbols[1]}")
self.__axes[2].set_xlabel('Time')
self.__axes[2].set_ylabel('Coefficient')
self.__axes[2].plot(times, coefficients)
self.__axes[2].set_ylim([-1, 1])
self.__axes[2].xaxis.set_major_formatter(self.__tick_fmt_time)
plt.setp(self.__axes[2].xaxis.get_majorticklabels(), rotation=45)
# Layout with padding between charts
self.__fig.tight_layout(pad=0.5)
# Redraw canvas
self.__canvas.draw()
+21 -14
View File
@@ -35,18 +35,18 @@ class MT5:
# Connect to MetaTrader5. Opens if not already open.
# Logger
self.log = logging.getLogger(__name__)
self.__log = logging.getLogger(__name__)
# Open MT5 and log error if it could not open
if not MetaTrader5.initialize():
self.log.error("initialize() failed")
self.__log.error("initialize() failed")
MetaTrader5.shutdown()
# Print connection status
self.log.debug(MetaTrader5.terminal_info())
self.__log.debug(MetaTrader5.terminal_info())
# Print data on MetaTrader 5 version
self.log.debug(MetaTrader5.version())
self.__log.debug(MetaTrader5.version())
def __del__(self):
# shut down connection to the MetaTrader 5 terminal
@@ -67,7 +67,7 @@ class MT5:
# Log symbol counts
total_symbols = MetaTrader5.symbols_total()
num_selected_symbols = len(selected_symbols)
self.log.debug(f"{num_selected_symbols} of {total_symbols} available symbols in Market Watch.")
self.__log.debug(f"{num_selected_symbols} of {total_symbols} available symbols in Market Watch.")
return selected_symbols
@@ -102,13 +102,16 @@ class MT5:
:return: Price data for symbol as dataframe
"""
prices_dataframe = None
# Get prices from MT5
prices = MetaTrader5.copy_rates_range(symbol, timeframe, from_date, to_date)
self.log.debug(f"{len(prices)} prices retrieved for {symbol}.")
if prices is not None:
self.__log.debug(f"{len(prices)} prices retrieved for {symbol}.")
# Create dataframe from data and convert time in seconds to datetime format
prices_dataframe = pd.DataFrame(prices)
prices_dataframe['time'] = pd.to_datetime(prices_dataframe['time'], unit='s')
# Create dataframe from data and convert time in seconds to datetime format
prices_dataframe = pd.DataFrame(prices)
prices_dataframe['time'] = pd.to_datetime(prices_dataframe['time'], unit='s')
return prices_dataframe
@@ -121,19 +124,23 @@ class MT5:
:return: Tick data for symbol as dataframe
"""
ticks_dataframe = None
# Get ticks from MT5
ticks = MetaTrader5.copy_ticks_range(symbol, from_date, to_date, MetaTrader5.COPY_TICKS_ALL)
# If ticks is None, there was an error
if ticks is None:
error = MetaTrader5.last_error()
self.log.error(f"Error retrieving ticks for {symbol}: {error}")
return None
self.__log.error(f"Error retrieving ticks for {symbol}: {error}")
else:
self.log.debug(f"{len(ticks)} ticks retrieved for {symbol}.")
self.__log.debug(f"{len(ticks)} ticks retrieved for {symbol}.")
# Create dataframe from data and convert time in seconds to datetime format
ticks_dataframe = pd.DataFrame(ticks)
ticks_dataframe['time'] = pd.to_datetime(ticks_dataframe['time'], unit='s')
try:
ticks_dataframe = pd.DataFrame(ticks)
ticks_dataframe['time'] = pd.to_datetime(ticks_dataframe['time'], unit='s')
except RecursionError:
self.__log.warning("Error converting ticks to dataframe.")
return ticks_dataframe