# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. # Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re import matplotlib import numpy as np import pandas as pd from base64 import b64encode from datetime import date, datetime, timedelta from pandas.plotting import register_matplotlib_converters from clr import AddReference AddReference("System") from System import * register_matplotlib_converters() matplotlib.use('Agg') font = {'family': 'DejaVu Sans'} matplotlib.rc('font',**font) matplotlib.rc('axes', edgecolor='#d5d5d5') import matplotlib.pyplot as plt import matplotlib.ticker as ticker import matplotlib.colors as mcolors from matplotlib.dates import DateFormatter from matplotlib.ticker import MaxNLocator, NullFormatter, ScalarFormatter, FormatStrFormatter la = matplotlib.font_manager.FontManager() lu = matplotlib.font_manager.FontProperties(family = "Open Sans Condensed") class ReportCharts: color_map = { "Equity": "#ff9914", "Option": "#DAA520", "Commodity": "#9400D3", "Forex": "#6495ED", "Future": "#808000", "Cfd": "#FFD700", "Crypto": "#FFDAB9", "FutureOption": "#1ED3A9", "IndexOption": "#A4AACC", "CryptoFuture": "#E55812" } def fig_to_base64(self, filename = '', fig = None, dpi = 200): base64 = 'data:image/png;base64,' if fig is not None: fig.savefig(filename, dpi=dpi, bbox_inches='tight') with open(filename, "rb") as fp: base64 += b64encode(fp.read()).decode('utf-8').replace('\n', '') return base64 def GetReturnsPerTrade(self, returns_per_trade = [], live_returns_per_trade = [], name = "returns-per-trade.png", width = 7, height = 5, live_color = "#ff9914", backtest_color = "#71c3fc"): if len(returns_per_trade) == 0: fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=30, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 if len(live_returns_per_trade) > 0: width = 11.5 height = 5 plt.figure() fig, ax = plt.subplots(1, 2, tight_layout=True) ax[0].hist(returns_per_trade, bins=75, color=backtest_color) ax[1].hist(live_returns_per_trade, bins=25, color=live_color) for i in range(2): if i == 0: ax[i].set_ylabel('Backtest', fontweight='demibold') ax[i].axvline(x=np.median(returns_per_trade), color="red", ls="dashed", label="median", linewidth=0.5) else: ax[i].set_ylabel('Live', fontweight='demibold') ax[i].axvline(x=np.median(live_returns_per_trade), color="red", ls="dashed", label="median", linewidth=0.5) ax[i].tick_params(labelsize=8) ax[i].tick_params(axis='x', color='#d5d5d5') ax[i].tick_params(axis='y', color='#d5d5d5') plt.setp(ax[i].spines.values(), color='#d5d5d5') ax[i].spines['right'].set_visible(False) ax[i].spines['top'].set_visible(False) else: fig = plt.figure() plt.hist(returns_per_trade, bins=75, color=backtest_color) plt.xticks(fontsize=8) plt.yticks(fontsize=8) plt.gca().spines['right'].set_visible(False) plt.gca().spines['top'].set_visible(False) plt.gca().tick_params(axis='x', color='#d5d5d5') plt.gca().tick_params(axis='y', color='#d5d5d5') plt.gca().axvline(x=np.median(returns_per_trade), color="red", ls="dashed", label="median", linewidth=0.5) plt.ylabel('') # Set the x ticks as percentage to keep consistency plt.xticks(ticks=plt.xticks()[0], labels=["{:.2f}%".format(tick * 100) for tick in plt.xticks()[0]]) fig.set_size_inches(width, height) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 def GetCumulativeReturns(self, data = None, live_data = None, benchmark_symbol = 'SPY', name = "cumulative-return.png", width = 11.5, height = 2.5, live_color = "#ff9914", backtest_color = "#71c3fc", gray = "#b3bcc0"): ''' data: [ [strategyTime], [strategyPoints], [benchTime], [benchResults] ] live_data: [ [strategyTime], [strategyPoints], [benchTime], [benchResults] ] ''' # Initialize lists here instead of method signature to avoid # unintended behavior when calling this method twice if data is None: data = [[],[],[],[]] if live_data is None: live_data = [[],[],[],[]] if len(data[0]) == 0: fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=20, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 plt.figure() ax = plt.gca() labels = ['Backtest', 'Benchmark'] labels_removed = [] rectangles = [] colors = [backtest_color, gray] values = [[data[0], data[1]], [data[2], data[3]]] for i, array in enumerate(values): if any(array[0]): ax.plot(array[0], array[1], linewidth=0.5, color=colors[i], drawstyle='steps-post') else: # We have nothing for this graph. Wipe any mention of it labels_removed.append(labels[i]) rectangles.append(plt.Rectangle((0, 0), 1, 1, fc=colors[i])) # Only get the labels we didn't remove (i.e. labels that have a graph, guaranteed) labels = [label for label in labels if label not in labels_removed] # Return if we don't have any valid labels if not any(labels): fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=20, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 live_labels = [] live_labels_removed = [] if len(live_data[0]) > 0: colors = [live_color, gray] labels.append('Live') values = [[live_data[0], live_data[1]], [live_data[2], live_data[3]]] for i, array in enumerate(values): if any(array[0]): ax.plot(array[0], array[1], linewidth=0.5, color=colors[i], drawstyle='steps-post') rectangles.append(plt.Rectangle((0, 0), 1, 1, fc=colors[i])) ax.legend(rectangles, labels, handlelength=0.8, handleheight=0.8, frameon=False, fontsize=8, ncol=len(labels)) fig = ax.get_figure() plt.xticks(rotation=0, ha='center', fontsize=8) plt.yticks(fontsize=8) ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) ax.yaxis.set_major_formatter(ticker.PercentFormatter()) ax.yaxis.set_major_locator(MaxNLocator(6)) plt.axhline(y=0, color='#d5d5d5', zorder=1) plt.setp(ax.spines.values(), color='#d5d5d5') ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') plt.ylabel("") plt.xlabel("") ax.yaxis.grid(True, color="#ececec") fig.set_size_inches(width, height) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 def GetDailyReturns(self, returns = [[],[]], live_returns = [[],[]], name = "daily-returns.png", width = 11.5, height = 2.5, live_color = "#ff9914", backtest_color = "#71c3fc", gray = "#b3bcc0"): if len(returns[0]) == 0: fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=20, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 returns[0] = list(returns[0]) returns[1] = list(returns[1]) live_returns[0] = list(live_returns[0]) live_returns[1] = list(live_returns[1]) plt.figure() ax = plt.gca() backtest_series = pd.Series(returns[1], index=returns[0]) live_series = pd.Series(live_returns[1], index=live_returns[0]) backtest_positive = backtest_series[backtest_series > 0] backtest_negative = backtest_series[backtest_series < 0] live_positive = live_series[live_series > 0] live_negative = live_series[live_series < 0] # Backtest #ax.bar(returns[0][:min(len(returns[0]),len(returns[1]))], returns[1], color=backtest_color,zorder=2) ax.bar(backtest_positive.index, backtest_positive.values, color=backtest_color, zorder=2) ax.bar(backtest_negative.index, backtest_negative.values, color=gray, zorder=2) # Live #ax.bar(live_returns[0][:min(len(live_returns[0]),len(live_returns[1]))], live_returns[1], color=live_color,zorder=2) ax.bar(live_positive.index, live_positive.values, color=live_color, zorder=2) ax.bar(live_negative.index, live_negative.values, color=gray, zorder=2) # Need to handle this since we don't use a legend if it is only backtesting if len(live_returns[0]) > 0: rectangles = [plt.Rectangle((0, 0), 1, 1, fc=backtest_color), plt.Rectangle((0, 0), 1, 1, fc=live_color)] ax.legend(rectangles, [label for label in ['Backtest', "Live"]], handlelength=0.8, handleheight=0.8, frameon=False, fontsize=8) fig = ax.get_figure() ax.xaxis_date() #ax.set_xticks(fontsize = 8) #ax.set_yticks(fontsize = 8) ax.set_ylabel("") ax.set_xlabel("") ax.yaxis.set_major_formatter(ticker.PercentFormatter()) ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) plt.axhline(y = 0, color = '#d5d5d5') plt.setp(ax.spines.values(), color='#d5d5d5') plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) ax.set_axisbelow(True) ax.yaxis.grid(True, color = "#ececec") fig.set_size_inches(width, height) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() return base64 def GetMonthlyReturns(self, returns = {}, live_returns = {}, width=7, height=5, name='monthly-returns.png'): ''' Expects monthly returns in dictionary keyed by year containing a list of monthly returns (as percentage values, i.e. 1% is 1.0 in the list). Example: {'2019': [10.0, 15.25, -20.05, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN]} ''' months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] # Populate the list with np.nan so that we can successfully # convert this dict into a DataFrame #for k in returns.keys(): while len(returns[k]) != 12: # returns[k].append(np.nan) if len(returns) == 0: print("No monthly returns found") fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=30, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 # Make data frame returns = pd.DataFrame(returns, index = months).transpose() c = mcolors.ColorConverter().to_rgb colors = [c('#CC0000'), c('#FF0000'), c('#FF3333'), c('#FF9933'), c('#FFFF66'), c('#FFFF99'), c('#B2FF66'), c('#99FF33'), c('#00FF00'), c('#00CC00')] abs_cmap = matplotlib.colors.LinearSegmentedColormap.from_list('monthly_returns', colors) norm = plt.Normalize(-10, 10) if len(live_returns) > 0: live_returns = pd.DataFrame(live_returns, index=months).transpose() fig, ax = plt.subplots(2, 1, gridspec_kw={'height_ratios': [6, 1]}) #ax[0].matshow(returns, aspect='auto', cmap=c_map, interpolation='none', vmin=-10, vmax=10) #ax[1].matshow(live_returns, aspect='auto', cmap=live_c_map, interpolation='none') ax[0].matshow(returns, aspect='auto', cmap=abs_cmap, norm=norm, interpolation='none') ax[1].matshow(live_returns, aspect='auto', cmap=abs_cmap, norm=norm, interpolation='none') ax[0].xaxis.set_major_locator(ticker.MaxNLocator(min(12, len(returns.columns)))) ax[0].yaxis.set_major_locator(ticker.MaxNLocator(len(returns.index.values))) ax[0].set_yticklabels([''] + list(returns.index.values)) ax[0].set_xticklabels([''] + [x for x in returns.columns]) ax[0].tick_params(labelsize=8, bottom=True, labelbottom=True, top=False, labeltop=False) ax[0].set_ylabel('Backtest', rotation='vertical', fontweight='black') for (j, i), label in np.ndenumerate(returns): if np.isnan(label): ax[0].text(i, j, "", ha='center', va='center', fontsize=7) else: ax[0].text(i, j, round(label, 1), ha='center', va='center', fontsize=7) ax[1].xaxis.set_major_locator(ticker.MaxNLocator(min(12, len(live_returns.columns)))) ax[1].yaxis.set_major_locator(ticker.MaxNLocator(len(live_returns.index.values))) ax[1].set_xticklabels([''] + [x for x in live_returns.columns]) ## will need to be fixed for more than 1 year ax[1].set_yticklabels([''] + list(live_returns.index.values)) ax[1].tick_params(labelsize=8, bottom=True, labelbottom=True, top=False, labeltop=False) ax[1].set_ylabel('Live', rotation='vertical', fontweight='black') for (j, i), label in np.ndenumerate(live_returns): if np.isnan(label): ax[1].text(i, j, "", ha='center', va='center', fontsize=7) else: ax[1].text(i, j, round(label, 1), ha='center', va='center', fontsize=7) ax[0].tick_params(axis='x', color='#d5d5d5') ax[0].tick_params(axis='y', color='#d5d5d5') ax[1].tick_params(axis='x', color='#d5d5d5') ax[1].tick_params(axis='y', color='#d5d5d5') else: ax = plt.imshow(returns, aspect='auto', cmap=abs_cmap, norm=norm, interpolation='none') fig = ax.get_figure() plt.xlabel('') plt.ylabel('') plt.gca().tick_params(axis='x', color='#d5d5d5') plt.gca().tick_params(axis='y', color='#d5d5d5') plt.yticks(range(len(returns.index.values)), returns.index.values, fontsize=8) plt.xticks(range(12), ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]) for (j, i), label in np.ndenumerate(returns): if np.isnan(label): plt.text(i, j, "", ha='center', va='center', fontsize=7) else: plt.text(i, j, str(round(label, 1)), ha='center', va='center', fontsize=7) fig.set_size_inches(width, height) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 def GetAnnualReturns(self, data = None, live_data = None, name = "annual-returns.png",width = 3.5*2, height = 2.5*2): live_color = "#ff9914" backtest_color = "#71c3fc" if data is None: data = [[], []] if live_data is None: live_data = [[], []] if len(data[0]) == 0: fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=30, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 # Cast to list just in case time = list(data[0]) + list(live_data[0]) returns = list(data[1]) + list(live_data[1]) plt.figure() ax = plt.gca() # Prevent value speculation on the y-axis ticks by # converting to string before plotting. ax.barh([str(i) for i in time], returns, color = [backtest_color], zorder=1) # Add a percentage sign at the end of each x-axis tick ax.xaxis.set_major_formatter(ticker.PercentFormatter()) fig = ax.get_figure() plt.xticks(rotation=0, ha='center', fontsize=8) plt.yticks(fontsize=8) plt.axvline(x=0, color='#d5d5d5', linewidth=0.5) vline = plt.axvline(x=np.mean(returns), color="red", ls="dashed", label="mean", linewidth=1) plt.legend([vline], ["mean"], loc='upper right', frameon=False, fontsize=8) plt.setp(ax.spines.values(), color='#d5d5d5') plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') ax.grid(color='#d5d5d5', axis='x', linewidth=1, zorder=0) ax.set_axisbelow(True) ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) plt.xlabel("") plt.ylabel("") ax.xaxis.grid(True) fig.set_size_inches(width, height) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 def GetDrawdown(self, data = [[],[]], live_data = [[],[]], worst = [{}], name = "drawdowns.png", width = 11.5, height = 2.5, gray = "#b3bcc0"): if len(data[0]) == 0: fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=20, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 time = list(data[0]) + list(live_data[0]) drawdown = list(data[1]) + list(live_data[1]) colors = ["#FFCCCCCC", "#FFE5CCCC", "#FFFFCCCC", "#E5FFCCCC", "#CCFFCCCC"] labels = ["1st Worst", "2nd Worst", "3rd Worst", "4th Worst", "5th Worst"] plt.figure() ax = plt.gca() ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) # Backtest #ax.plot(time, drawdown, color=gray, zorder=2) ax.fill_between(time, drawdown, 0, color=gray, zorder=3, step='post') for index, values in enumerate(worst): start = values['Begin'] end = values['End'] if start == end: worst_point = start else: sub_data = drawdown[time.index(start):time.index(end)] worst_point = time[drawdown.index(min(sub_data))] plt.axvspan(start, end, 0, 0.95, color = colors[index], zorder = 1) plt.axvline(worst_point, 0, 0.95, ls = 'dashed', color = 'black', zorder = 4, linewidth = 0.5) ax.text(worst_point, min(drawdown) * 0.75, labels[index], rotation = 90, zorder = 4, va='bottom') # Live live_time = live_data[0] live_drawdown = live_data[1] # No need to draw the live mode stuff since we've already taken care of it. # We're just after the Live trading dotted plot in case it exists plt.axvline(live_time[0], 0, 0.95, ls='dotted', color='red', zorder=4) if len(live_time) > 0 else None plt.text(live_time[0], min(min(drawdown), min(live_drawdown)) * 0.75, "Live Trading", rotation=90, zorder=4, fontsize=7) if len(live_time) > 0 else None fig = ax.get_figure() plt.xticks(rotation=0, ha='center', fontsize=8) plt.yticks(ticks=[i for i in plt.yticks()[0] if i <= 0], labels=['{:.1f}%'.format(i * 100) for i in plt.yticks()[0] if i <= 0], fontsize=8) plt.ylabel("") plt.xlabel("") plt.axhline(y=0, color='#d5d5d5', zorder=1) plt.setp(ax.spines.values(), color='#d5d5d5') plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) ax.yaxis.grid(True, color="#ececec") fig.set_size_inches(width, height) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 def GetCrisisEventsPlots(self, data = [[],[],[]], name = '', width = 7, height = 5, backtest_color = "#71c3fc", gray = "#b3bcc0"): if len(data[0]) == 0: fig = plt.figure() fig.set_size_inches(width, height) base64 = self.fig_to_base64(f'{name}.png', fig) plt.cla() plt.clf() plt.close('all') return base64 plt.figure() ax = plt.gca() fig = ax.get_figure() ax.xaxis.set_major_formatter(DateFormatter("%Y-%m-%d")) colors = [backtest_color, gray] for j, values in enumerate(data[1:]): ax.plot(data[0][:min(len(data[0]),len(values))], values, color=colors[j], linewidth=0.5, zorder=2, drawstyle='steps-post') labels = ['Backtest', 'Benchmark'] rectangles = [plt.Rectangle((0, 0), 1, 1, fc=backtest_color), plt.Rectangle((0, 0), 1, 1, fc=gray)] leg = ax.legend(rectangles, labels, handlelength=0.8, handleheight=0.8, frameon=False, fontsize=8, ncol=len(labels)) for line in leg.get_lines(): line.set_linewidth(3) plt.axhline(y=0, color= gray, zorder=1) plt.setp(ax.spines.values(), color='#d5d5d5') ax.tick_params(axis='x', labelsize=8, labelrotation=45) plt.yticks(ticks=plt.yticks()[0], labels=['{0:g}%'.format(i * 100) for i in plt.yticks()[0]], fontsize=8) plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) plt.xlabel("") plt.ylabel("") ax.yaxis.grid(True, color="#ececec") fig.set_size_inches(width, height) base64 = self.fig_to_base64(f'{name}.png', fig) plt.cla() plt.clf() plt.close('all') return base64 def GetRollingBeta(self, data = [[],[],[],[]], live_data = [[],[],[],[]], name = "rolling-portfolio-beta-to-equity.png", width = 11.5, height = 2.5, live_six_months_color = "#ff9914", live_twelve_months_color = "#ffd700", backtest_six_months_color = "#71c3fc", backtest_twelve_months_color = "#1d7dc1"): if len(data[0]) == 0 and len(live_data[0]) == 0: fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=20, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 # Data will come in the following format: # [six month rolling beta time, six month rolling beta, twelve month rolling beta time, twelve month rolling beta] backtest_six_month_beta_dates, backtest_six_month_beta = (data[0], data[1]) backtest_twelve_month_beta_dates, backtest_twelve_month_beta = (data[2], data[3]) live_six_month_beta_dates, live_six_month_beta = (live_data[0], live_data[1]) live_twelve_month_beta_dates, live_twelve_month_beta = (live_data[2], live_data[3]) labels = [] rectangles = [] if len(backtest_six_month_beta) > 0: labels += ['6 mo.'] rectangles += [plt.Rectangle((0, 0), 1, 1, fc=backtest_six_months_color)] if len(backtest_twelve_month_beta) > 0: labels += ['12 mo.'] rectangles += [plt.Rectangle((0, 0), 1, 1, fc=backtest_twelve_months_color)] if len(live_six_month_beta) > 0: labels += ['Live 6 mo.'] rectangles += [plt.Rectangle((0, 0), 1, 1, fc=live_six_months_color)] if len(live_twelve_month_beta) > 0: labels += ['Live 12 mo.'] rectangles += [plt.Rectangle((0, 0), 1, 1, fc=live_twelve_months_color)] plt.figure() ax = plt.gca() fig = ax.get_figure() ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) # Backtest if len(backtest_six_month_beta) > 0: ax.plot(backtest_six_month_beta_dates, backtest_six_month_beta, linewidth=0.5, color=backtest_six_months_color) if len(backtest_twelve_month_beta) > 0: ax.plot(backtest_twelve_month_beta_dates, backtest_twelve_month_beta, linewidth=0.5, color=backtest_twelve_months_color) # Live if len(live_six_month_beta) > 0: ax.plot(live_six_month_beta_dates, live_six_month_beta, linewidth=0.5, color=live_six_months_color) if len(live_twelve_month_beta) > 0: ax.plot(live_twelve_month_beta_dates, live_twelve_month_beta, linewidth=0.5, color=live_twelve_months_color) leg = ax.legend(rectangles, labels, handlelength=0.8, handleheight=0.8, frameon=False, fontsize=8, ncol=2) for line in leg.get_lines(): line.set_linewidth(3) plt.axhline(y=0, color='#d5d5d5', zorder=1) plt.setp(ax.spines.values(), color='#d5d5d5') ax.tick_params(axis='both', labelsize=8, labelrotation=0) plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') plt.xlabel("") plt.ylabel("") ax.set_axisbelow(True) ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) ax.yaxis.grid(True, color="#ececec") fig.set_size_inches(width, height) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 def GetRollingSharpeRatio(self, data = [[],[]], live_data = [[],[]], name = "rolling-sharpe-ratio.png", width = 11.5, height = 2.5, live_six_months_color = "#ff9914", live_twelve_months_color = "#ffd700", backtest_six_months_color = "#71c3fc", backtest_twelve_months_color = "#1d7dc1"): if len(data[0]) == 0: fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=20, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 plt.figure() ax = plt.gca() ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) # Data will come in the following format: # [six month rolling sharpe time, six month rolling sharpe, twelve month rolling sharpe time, twelve month rolling sharpe] backtest_six_month_rolling_sharpe_dates, backtest_six_month_rolling_sharpe = (data[0], data[1]) backtest_twelve_month_rolling_sharpe_dates, backtest_twelve_month_rolling_sharpe = (data[2], data[3]) live_six_month_rolling_sharpe_dates, live_six_month_rolling_sharpe = (live_data[0], live_data[1]) live_twelve_month_rolling_sharpe_dates, live_twelve_month_rolling_sharpe = (live_data[2], live_data[3]) labels = [] rectangles = [] if len(backtest_six_month_rolling_sharpe) > 0: labels += ['6 mo.'] rectangles += [plt.Rectangle((0, 0), 1, 1, fc=backtest_six_months_color)] if len(backtest_twelve_month_rolling_sharpe) > 0: labels += ['12 mo.'] rectangles += [plt.Rectangle((0, 0), 1, 1, fc=backtest_twelve_months_color)] if len(live_six_month_rolling_sharpe) > 0: labels += ['Live 6 mo.'] rectangles += [plt.Rectangle((0, 0), 1, 1, fc=live_six_months_color)] if len(live_twelve_month_rolling_sharpe) > 0: labels += ['Live 12 mo.'] rectangles += [plt.Rectangle((0, 0), 1, 1, fc=live_twelve_months_color)] # Backtest if len(backtest_six_month_rolling_sharpe) > 0: ax.plot(backtest_six_month_rolling_sharpe_dates, backtest_six_month_rolling_sharpe, linewidth=0.5, color=backtest_six_months_color) if len(backtest_twelve_month_rolling_sharpe) > 0: ax.plot(backtest_twelve_month_rolling_sharpe_dates, backtest_twelve_month_rolling_sharpe, linewidth=0.5, color=backtest_twelve_months_color) # Live if len(live_six_month_rolling_sharpe) > 0: ax.plot(live_six_month_rolling_sharpe_dates, live_six_month_rolling_sharpe, linewidth=0.5, color=live_six_months_color) if len(live_twelve_month_rolling_sharpe) > 0: ax.plot(live_twelve_month_rolling_sharpe_dates, live_twelve_month_rolling_sharpe, linewidth=0.5, color=live_twelve_months_color) leg = ax.legend(rectangles, labels, handlelength=0.8, handleheight=0.8, frameon=False, fontsize=8) for line in leg.get_lines(): line.set_linewidth(3) plt.axhline(y=0, color='#d5d5d5', zorder=1) plt.setp(ax.spines.values(), color='#d5d5d5') ax.tick_params(axis='both', labelsize=8, labelrotation=0) plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') plt.ylabel("") plt.xlabel("") ax.set_axisbelow(True) ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) ax.yaxis.grid(True, color="#ececec") fig = ax.get_figure() fig.set_size_inches(width, height) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 def GetAssetAllocation(self, data = [[],[]], live_data = [[],[]], name="asset-allocation.png", width = 7, height = 5): if len(data[0]) == 0: fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=30, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return {"Backtest Asset Allocation": base64} symbols = [data[0], live_data[0]] data = [data[1], live_data[1]] colors = ['#fce0bd', '#fcd6a7', '#fbcd92', '#fac37c', '#f8af53', '#f79b31', '#de8b2c', "#dde1e3"] pies = {} for i in range(len(data)): symbols_to_use, to_label = symbols[i], data[i] # No need to plot if there are no symbols/data -- necessary as we don't want to return a dictionary # with even a blank plot for live if only using a backtest if len(symbols_to_use) == 0: continue to_label = to_label[:7] symbols_to_use = symbols_to_use[:7] if sum(to_label) < 1: to_label.append(1 - sum(to_label)) symbols_to_use.append('Others') labels = [f'{symbol}\n' + '{:.2f}%'.format(value * 100) for symbol, value in zip(symbols_to_use, to_label)] fig = plt.figure() plt.pie(to_label, colors = colors) plt.legend(labels, frameon = False, fontsize = 8, loc = 'center left', bbox_to_anchor=(0, 0.5)) plt.axis('equal') fig.set_size_inches(width, height) if i == 0: pies["Backtest Asset Allocation"] = self.fig_to_base64(f"asset-allocation-backtest.png", fig) else: pies["Live Asset Allocation"] = self.fig_to_base64(f"asset-allocation-live.png", fig) plt.cla() plt.clf() plt.close('all') pies["filler"] = '' return pies def GetLeverage(self, data = [[],[]], live_data = [[],[]], name = "leverage.png",width = 11.5, height = 2.5, backtest_color = "#71c3fc", live_color = "#ff9914",): if len(data[0]) == 0: fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=20, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 labels = ['Backtest'] plt.figure() ax = plt.gca() fig = ax.get_figure() # Backtest ax.fill_between(data[0], 0, data[1], color = backtest_color, alpha = 0.75, step='post') # Live if len(live_data[0]) != 0: labels.append('Live') ax.fill_between(live_data[0], 0, live_data[1], color=live_color, alpha=0.75, step = 'post') rectangles = [plt.Rectangle((0, 0), 1, 1, fc=backtest_color), plt.Rectangle((0, 0), 1, 1, fc=live_color)] ax.legend(rectangles, [label for label in labels], handlelength=0.8, handleheight=0.8, frameon=False, fontsize=8) ax.set_xticklabels(ax.get_xticklabels(), rotation=0, ha='center') ax.tick_params(axis='both', labelsize=8, labelrotation=0) ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) plt.axhline(y=0, color='#d5d5d5') plt.setp(ax.spines.values(), color='#d5d5d5') plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') plt.ylabel("") plt.xlabel("") ax.set_axisbelow(True) ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) ax.yaxis.grid(True, color="#ececec") fig.set_size_inches(width, height) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 def GetExposure(self, time = [], long_securities = [], short_securities = [], long_data = [[]], short_data = [[]], live_time = [], live_long_securities = [], live_short_securities = [], live_long_data = [[]], live_short_data = [[]], name = "exposure.png", width = 11.5, height = 2.5): if len(time) == 0: fig = plt.figure() fig.set_size_inches(width, height) left, box_width = .25, .5 bottom, box_height = .25, .5 right = left + box_width top = bottom + box_height ax = fig.add_axes([0, 0, 1, 1]) ax.text(0.5 * (left + right), 0.5 * (top + bottom), 'Insufficient Data', color="#d5d5d5", horizontalalignment='center', verticalalignment='center', fontsize=20, transform=ax.transAxes) ax.axis('off') for _, spine in ax.spines.items(): spine.set_visible(False) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64 for k, v in list(self.color_map.items()): self.color_map[k + ' - Short'] = '#' + hex(int(v[1:], 16) ^ 0xffffff)[2:].zfill(6) # None if no colors can be mapped, so stackplot gets None and doesn't try to access this color list long_colors = [self.color_map[security] for security in long_securities] if len(long_securities) > 0 else None long_live_colors = [self.color_map[security] for security in live_long_securities] if len(live_long_securities) > 0 else None short_colors = [self.color_map[security + ' - Short'] for security in short_securities] if len(short_securities) > 0 else None short_live_colors = [self.color_map[security + ' - Short'] for security in live_short_securities] if len(live_short_securities) > 0 else None ax = plt.gca() # Create step plot for the stackplot by adding a value # right before the next data point with the same previous value time_copy = [] long_data_copy = [] short_data_copy = [] j = 0 for time_idx, longs, shorts in zip(time, long_data, short_data): long_data_copy.append([]) short_data_copy.append([]) long_len = len(longs) for i in range(1, long_len + 1): if i == long_len : time_copy.append(time[i - 1]) long_data_copy[j].append(longs[i - 1]) short_data_copy[j].append(shorts[i - 1]) else: time_copy.append(time[i - 1]) time_copy.append(time[i]) long_data_copy[j].append(longs[i - 1]) long_data_copy[j].append(longs[i - 1]) short_data_copy[j].append(shorts[i - 1]) short_data_copy[j].append(shorts[i - 1]) j += 1 if len([x for x in long_data]) == 0: long_data = [[]] if len([x for x in short_data]) == 0: short_data = [[]] if len([x for x in live_long_data]) == 0: live_long_data = [[]] if len([x for x in live_short_data]) == 0: live_short_data = [[]] # Create step plot for the stackplot by adding a value # right before the next data point with the same previous value live_time_copy = [] live_long_data_copy = [] live_short_data_copy = [] j = 0 for time_idx, longs, shorts in zip(live_time, live_long_data, live_short_data): live_long_data_copy.append([]) live_short_data_copy.append([]) long_len = len(longs) for i in range(1, long_len + 1): if i == long_len : live_time_copy.append(live_time[i - 1]) live_long_data_copy[j].append(longs[i - 1]) live_short_data_copy[j].append(shorts[i - 1]) else: live_time_copy.append(live_time[i - 1]) live_time_copy.append(live_time[i]) live_long_data_copy[j].append(longs[i - 1]) live_long_data_copy[j].append(longs[i - 1]) live_short_data_copy[j].append(shorts[i - 1]) live_short_data_copy[j].append(shorts[i - 1]) j += 1 if not len(long_data_copy) > 0: long_data_copy.append([]) if not len(short_data_copy) > 0: short_data_copy.append([]) if not len(live_long_data_copy) > 0: live_long_data_copy.append([]) if not len(live_short_data_copy) > 0: live_short_data_copy.append([]) if not len(time_copy) > 0: time_copy.append(float('nan')) # No need to check if live is empty or not, this will handle it, just needs to plot whichever has the longer time index first if max([len(x) for x in long_data_copy]) > max([len(x) for x in short_data_copy]): ax.stackplot(time_copy[:max([len(x) for x in long_data_copy])], np.vstack(long_data_copy), colors=long_colors, alpha = 0.75) ax.stackplot(time_copy[:max([len(x) for x in short_data_copy])], np.vstack(short_data_copy), colors=short_colors, alpha=0.75) else: ax.stackplot(time_copy[:max([len(x) for x in short_data_copy])], np.vstack(short_data_copy), colors=short_colors, alpha=0.75) ax.stackplot(time_copy[:max([len(x) for x in long_data_copy])], np.vstack(long_data_copy), colors=long_colors, alpha=0.75) if max([len(x) for x in live_long_data_copy]) > max([len(x) for x in live_short_data_copy]): ax.stackplot(live_time_copy[:max([len(x) for x in live_long_data_copy])], np.vstack(live_long_data_copy), colors=long_live_colors, alpha = 0.75) ax.stackplot(live_time_copy[:max([len(x) for x in live_short_data_copy])], np.vstack(live_short_data_copy), colors=short_live_colors, alpha = 0.75) else: ax.stackplot(live_time_copy[:max([len(x) for x in live_short_data_copy])], np.vstack(live_short_data_copy), colors=short_live_colors, alpha=0.75) ax.stackplot(live_time_copy[:max([len(x) for x in live_long_data_copy])], np.vstack(live_long_data_copy), colors=long_live_colors, alpha=0.75) labels = long_securities + short_securities live_labels = live_long_securities + live_short_securities for security in short_securities: if not all([all([abs(y) == 0.0 for y in x]) for x in short_data]): labels.append(security + ' - Short') for security in live_short_securities: if not all([all([abs(y) == 0.0 for y in x]) for x in live_short_data]): live_labels.append(security + ' - Short') # use dict.fromkeys() instead of set() to remove duplicates and preserve order labels = list(dict.fromkeys(labels)) live_labels = list(dict.fromkeys(live_labels)) rectangles = [plt.Rectangle((0, 0), 1, 1, fc=self.color_map[lab]) for lab in labels] live_rectangles = [plt.Rectangle((0, 0), 1, 1, fc=self.color_map[lab]) for lab in live_labels] ax.legend(rectangles + live_rectangles, labels + [f'{lab} - Live' for lab in live_labels], handlelength=0.8, handleheight=0.8, frameon=False, fontsize=8, ncol=len(labels), loc='upper right') fig = ax.get_figure() plt.xticks(rotation = 0,ha = 'center', fontsize = 8) plt.yticks(fontsize = 8) plt.xlabel("") ax.axhline(y=0, color = 'black', linewidth = 0.5) ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) plt.setp(ax.spines.values(), color='#d5d5d5') ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') plt.ylabel("") plt.xlabel("") ax.set_axisbelow(True) ax.yaxis.grid(True, color = "#ececec") fig.set_size_inches(width, height) base64 = self.fig_to_base64(name, fig) plt.cla() plt.clf() plt.close('all') return base64