from IPython.display import display
from IPython.display import HTML
import IPython.core.display as di # Example: di.display_html('<h3>%s:</h3>' % str, raw=True)
# This line will hide code by default when the notebook is exported as HTML
di.display_html('<script>jQuery(function() {if (jQuery("body.notebook_app").length == 0) { jQuery(".input_area").toggle(); jQuery(".prompt").toggle();}});</script>', raw=True)
# This line will add a button to toggle visibility of code blocks, for use with the HTML export version
di.display_html('''<button onclick="jQuery('.input_area').toggle(); jQuery('.prompt').toggle();">Código ON/OFF</button>''', raw=True)
A Carteira 4Fundos DEF é composta por:
A Carteira 4Fundos EW é composta por:
A Carteira 4Fundos AGR é composta por:
# importing libraries
import matplotlib.pyplot as plt
import plotly.graph_objs as go
import plotly.offline as py
import cufflinks as cf
import seaborn as sns
import pandas as pd
import numpy as np
import quandl
import plotly
import time
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
from IPython.display import Markdown, display
from matplotlib.ticker import FuncFormatter
from pandas.core.base import PandasObject
from datetime import datetime
# Setting pandas dataframe display options
pd.set_option("display.max_rows", 20)
pd.set_option('display.width', 800)
pd.set_option('max_colwidth', 800)
pd.options.display.float_format = '{:,.2f}'.format
# Set plotly offline
init_notebook_mode(connected=True)
# Set matplotlib style
plt.style.use('seaborn')
# Set cufflinks offline
cf.go_offline()
# Defining today's Date
from datetime import date
today = date.today()
#### Functions ####
def compute_growth_rate(dataframe, initial_value=100, initial_cost=0, ending_cost=0):
initial_cost = initial_cost / 100
ending_cost = ending_cost / 100
GR = ((1 + dataframe.pct_change()).cumprod()) * (initial_value * (1 - initial_cost))
GR.iloc[0] = initial_value * (1 - initial_cost)
GR.iloc[-1] = GR.iloc[-1] * (1 * (1 - ending_cost))
return GR
def compute_drawdowns(dataframe):
'''
Function to compute drawdowns of a timeseries
given a dataframe of prices
'''
return (dataframe / dataframe.cummax() -1) * 100
def compute_return(dataframe):
'''
Function to compute drawdowns of a timeseries
given a dataframe of prices
'''
return (dataframe.iloc[-1] / dataframe.iloc[0] -1) * 100
def compute_max_DD(dataframe):
return compute_drawdowns(dataframe).min()
def compute_cagr(dataframe, years=0, investment_value=0):
'''
Function to calculate CAGR given a dataframe of prices
'''
years = len(pd.date_range(dataframe.index[0], dataframe.index[-1], freq='D')) / 365
if investment_value == 0:
return (dataframe.iloc[-1].div(dataframe.iloc[0]).pow(1 / years)).sub(1).mul(100)
else:
return (dataframe.iloc[-1].div(investment_value).pow(1 / years)).sub(1).mul(100)
def compute_mar(dataframe):
'''
Function to calculate mar: Return Over Maximum Drawdown
given a dataframe of prices
'''
return compute_cagr(dataframe).div(compute_drawdowns(dataframe).min().abs())
def compute_StdDev(dataframe, freq='days'):
'''
Function to calculate annualized standart deviation
given a dataframe of prices. It takes into account the
frequency of the data.
'''
if freq == 'days':
return dataframe.pct_change().std().mul((np.sqrt(252))).mul(100)
if freq == 'months':
return dataframe.pct_change().std().mul((np.sqrt(12))).mul(100)
def compute_sharpe(dataframe, years=0, freq='days'):
'''
Function to calculate the sharpe ratio given a dataframe of prices.
'''
return compute_cagr(dataframe, years).div(compute_StdDev(dataframe, freq))
def compute_return(dataframe, investment_value=0):
'''
Function to compute drawdowns of a timeseries
given a dataframe of prices
'''
if investment_value == 0:
return(dataframe.iloc[-1] / dataframe.iloc[0] -1) * 100
else:
return(dataframe.iloc[-1] / investment_value -1) * 100
def compute_performance_table(dataframe, years='si', freq='days', investment_value=0):
'''
Function to calculate a performance table given a dataframe of prices.
Takes into account the frequency of the data.
'''
if years == 'si':
years = len(pd.date_range(dataframe.index[0], dataframe.index[-1], freq='D')) / 365
df = pd.DataFrame([compute_cagr(dataframe, years, investment_value), compute_StdDev(dataframe, freq),
compute_sharpe(dataframe, years, freq), compute_max_DD(dataframe), compute_mar(dataframe)])
df.index = ['CAGR', 'StdDev', 'Sharpe', 'Max DD', 'MAR']
df = round(df.transpose(), 2)
# Colocar percentagens
df['CAGR'] = (df['CAGR'] / 100).apply('{:.2%}'.format)
df['StdDev'] = (df['StdDev'] / 100).apply('{:.2%}'.format)
df['Max DD'] = (df['Max DD'] / 100).apply('{:.2%}'.format)
# Return object
return df
else:
df = pd.DataFrame([compute_cagr(dataframe, years, investment_value), compute_StdDev(dataframe, freq),
compute_sharpe(dataframe, years, freq), compute_max_DD(dataframe), compute_mar(dataframe)])
df.index = ['CAGR', 'StdDev', 'Sharpe', 'Max DD', 'MAR']
df = round(df.transpose(), 2)
# Colocar percentagens
df['CAGR'] = (df['CAGR'] / 100).apply('{:.2%}'.format)
df['StdDev'] = (df['StdDev'] / 100).apply('{:.2%}'.format)
# Return object
return df
def compute_time_period(timestamp_1, timestamp_2):
year = timestamp_1.year - timestamp_2.year
month = timestamp_1.month - timestamp_2.month
day = timestamp_1.day - timestamp_2.day
if month < 0:
year = year - 1
month = 12 + month
if day < 0:
day = - day
# Returns datetime object in years, month, days
return(str(year) + ' Years ' + str(month) + ' Months ' + str(day) + ' Days')
def filter_by_date(dataframe, years=0, previous_row=False):
last_date = dataframe.tail(1).index
year_nr = last_date.year.values[0]
month_nr = last_date.month.values[0]
day_nr = last_date.day.values[0]
new_date = str(year_nr - years) + '-' + str(month_nr) + '-' + str(day_nr)
if previous_row == False:
return dataframe.loc[new_date:]
elif previous_row == True:
return pd.concat([dataframe.loc[:new_date].tail(1), dataframe.loc[new_date:]])
def get(quotes):
# resample quotes to business month
monthly_quotes = quotes.resample('BM').last()
# get monthly returns
returns = monthly_quotes.pct_change()
# get close / first column if given DataFrame
if isinstance(returns, pd.DataFrame):
returns.columns = map(str.lower, returns.columns)
if len(returns.columns) > 1 and 'close' in returns.columns:
returns = returns['close']
else:
returns = returns[returns.columns[0]]
# get returnsframe
returns = pd.DataFrame(data={'Retornos': returns})
returns['Ano'] = returns.index.strftime('%Y')
returns['Mês'] = returns.index.strftime('%b')
# make pivot table
returns = returns.pivot('Ano', 'Mês', 'Retornos').fillna(0)
# order columns by month
returns = returns[['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']]
return returns
def plot(returns,
title="Monthly Returns (%)",
title_color="black",
title_size=12,
annot_size=10,
figsize=None,
cmap='RdYlGn',
cbar=False,
square=False):
returns = get(returns)
returns *= 100
if figsize is None:
size = list(plt.gcf().get_size_inches())
figsize = (size[0], size[0] // 2)
plt.close()
fig, ax = plt.subplots(figsize=figsize)
ax = sns.heatmap(returns, ax=ax, annot=True,
annot_kws={"size": annot_size}, fmt="0.2f", linewidths=0.4, center=0,
square=square, cbar=cbar, cmap=cmap)
ax.set_title(title, fontsize=title_size, color=title_color, fontweight="bold")
fig.subplots_adjust(hspace=0)
plt.yticks(rotation=0)
plt.show()
plt.close()
PandasObject.get_returns_heatmap = get
PandasObject.plot_returns_heatmap = plot
def calendarize(returns):
'''
The calendarize function is an slight adaption of ranaroussi's monthly-returns-heatmap
You can find it here: https://github.com/ranaroussi/monthly-returns-heatmap/
It turns monthly data into a 12 columns(months) and yearly row seaborn heatmap
'''
# get close / first column if given DataFrame
if isinstance(returns, pd.DataFrame):
returns.columns = map(str.lower, returns.columns)
if len(returns.columns) > 1 and 'close' in returns.columns:
returns = returns['close']
else:
returns = returns[returns.columns[0]]
# get returnsframe
returns = pd.DataFrame(data={'Retornos': returns})
returns['Ano'] = returns.index.strftime('%Y')
returns['Mês'] = returns.index.strftime('%b')
# make pivot table
returns = returns.pivot('Ano', 'Mês', 'Retornos').fillna(0)
# order columns by month
returns = returns[['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']]
return returns
def plotly_table(df, width=990, height=500, columnwidth=[25], title=None , index=True, header=True,
header_alignment=['center'], header_line_color='rgb(100, 100, 100)', header_font_size=[12],
header_font_color=['rgb(45, 45, 45)'], header_fill_color=['rgb(200, 200, 200)'],
cells_alignment=['center'], cells_line_color=['rgb(200, 200, 200)'], cells_font_size=[11],
cells_font_color=['rgb(45, 45, 45)'], cells_fill_color=['rgb(245, 245, 245)','white' ]):
# Making the header bold and conditional
if (header == False and index == False):
lst = list(df.columns[0 + i] for i in range(len(df.columns)))
header = [[i] for i in lst]
header = list([str( '<b>' + header[0 + i][0] + '</b>') for i in range(len(df.columns))])
header = [[i] for i in header]
header.pop(0)
header = [[]] + header
trace = go.Table(
columnwidth = columnwidth,
header=dict(values=header,
line = dict(color=header_line_color),
align = header_alignment,
font = dict(color=header_font_color, size=header_font_size),
height = 22,
fill = dict(color=header_fill_color)),
cells=dict(values=df.transpose().values.tolist(),
line=dict(color=cells_line_color),
align = cells_alignment,
height = 22,
font = dict(color=cells_font_color, size=cells_font_size),
fill = dict(color = [cells_fill_color * len(df.index)]),
),
)
# Making the header bold and conditional
if (header == True and index == True):
lst = list(df.columns[0 + i] for i in range(len(df.columns)))
header = [[i] for i in lst]
header = list([str( '<b>' + header[0 + i][0] + '</b>') for i in range(len(df.columns))])
header = [[i] for i in header]
header = [['']] + header
# Making the index Bold
lst_i = list(df.index[0 + i] for i in range(len(df.index)))
index = [[i] for i in lst_i]
index = list([[ '<b>' + str(index[0 + i][0]) + '</b>' for i in range(len(df.index))]])
trace = go.Table(
columnwidth = columnwidth,
header=dict(values=header,
line = dict(color=header_line_color),
align = header_alignment,
font = dict(color=header_font_color, size=header_font_size),
height = 22,
fill = dict(color=header_fill_color)),
cells=dict(values=index + df.transpose().values.tolist(),
line=dict(color=cells_line_color),
align = cells_alignment,
height = 22,
font = dict(color=cells_font_color, size=cells_font_size),
fill = dict(color = [cells_fill_color * len(df.index)]),
),
)
# Making the header bold and conditional
if (header == False and index == True):
lst = list(df.columns[0 + i] for i in range(len(df.columns)))
header = [[i] for i in lst]
header = list([str( '<b>' + header[0 + i][0] + '</b>') for i in range(len(df.columns))])
header = [[i] for i in header]
header = [[]] + header
lst_i = list(df.index[0 + i] for i in range(len(df.index)))
index = [[i] for i in lst_i]
index = list([[ '<b>' + str(index[0 + i][0]) + '</b>' for i in range(len(df.index))]])
trace = go.Table(
columnwidth = columnwidth,
header=dict(values=header,
line = dict(color=header_line_color),
align = header_alignment,
font = dict(color=header_font_color, size=header_font_size),
height = 22,
fill = dict(color=header_fill_color)),
cells=dict(values=index + df.transpose().values.tolist(),
line=dict(color=cells_line_color),
align = cells_alignment,
height = 22,
font = dict(color=cells_font_color, size=cells_font_size),
fill = dict(color = [cells_fill_color * len(df.index)]),
),
)
# Making the header bold and conditional
if (header == True and index == False):
lst = list(df.columns[0 + i] for i in range(len(df.columns)))
header = [[i] for i in lst]
header = list([str( '<b>' + header[0 + i][0] + '</b>') for i in range(len(df.columns))])
header = [[i] for i in header]
header = header
trace = go.Table(
columnwidth = columnwidth,
header=dict(values=header,
line = dict(color=header_line_color),
align = header_alignment,
font = dict(color=header_font_color, size=header_font_size),
height = 22,
fill = dict(color=header_fill_color)),
cells=dict(values=df.transpose().values.tolist(),
line=dict(color=cells_line_color),
align = cells_alignment,
height = 22,
font = dict(color=cells_font_color, size=cells_font_size),
fill = dict(color = [cells_fill_color * len(df.index)]),
),
)
if title == None:
layout = go.Layout(
autosize=False,
height=height,
width=width,
margin=dict (l=0, r=0, b=0, t=0, pad=0),
)
else:
layout = go.Layout(
autosize=False,
height=height,
width=width,
title=title,
margin=dict( l=0, r=0, b=0, t=25, pad=0),
)
data = [trace]
fig = go.Figure(data=data, layout=layout)
py.iplot(fig, show_link=False, config={'modeBarButtonsToRemove': ['sendDataToCloud','hoverCompareCartesian'],
'displayModeBar': False})
def compute_portfolio(quotes, weights, Nomes):
# Anos do Portfolio
Years = quotes.index.year.unique()
# Dicionário com Dataframes anuais das cotações dos quotes
Years_dict = {}
k = 0
for Year in Years:
# Dynamically create key
key = Year
# Calculate value
value = quotes.loc[str(Year)]
# Insert in dictionary
Years_dict[key] = value
# Counter
k += 1
# Dicionário com Dataframes anuais das cotações dos quotes
Quotes_dict = {}
Portfolio_dict = {}
k = 0
for Year in Years:
n = 0
#Setting Portfolio to be a Global Variable
global Portfolio
# Dynamically create key
key = Year
# Calculate value
if (Year-1) in Years:
value = Years_dict[Year].append(Years_dict[Year-1].iloc[[-1]]).sort_index()
else:
value = Years_dict[Year].append(Years_dict[Year].iloc[[-1]]).sort_index()
# Set beginning value to 100
value = (value / value.iloc[0]) * 100
#
for column in value.columns:
value[column] = value[column] * weights[n]
n +=1
# Get Returns
Returns = value.pct_change()
# Calculating Portfolio Value
value['Portfolio'] = value.sum(axis=1)
# Creating Weights_EOP empty DataFrame
Weights_EOP = pd.DataFrame()
# Calculating End Of Period weights
for Name in Nomes:
Weights_EOP[Name] = value[Name] / value['Portfolio']
# Calculating Beginning Of Period weights
Weights_BOP = Weights_EOP.shift(periods=1)
# Calculatins Portfolio Value
Portfolio = pd.DataFrame(Weights_BOP.multiply(Returns).sum(axis=1))
Portfolio.columns=['Simple']
# Transformar os simple returns em log returns
Portfolio['Log'] = np.log(Portfolio['Simple'] + 1)
# Cumsum() dos log returns para obter o preço do Portfolio
Portfolio['Price'] = 100*np.exp(np.nan_to_num(Portfolio['Log'].cumsum()))
Portfolio['Price'] = Portfolio['Price']
# Insert in dictionaries
Quotes_dict[key] = value
Portfolio_dict[key] = Portfolio
# Counter
k += 1
# Making an empty Dataframe for Portfolio data
Portfolio = pd.DataFrame()
for Year in Years:
Portfolio = pd.concat([Portfolio, Portfolio_dict[Year]['Log']])
# Delete repeated index values in Portfolio
Portfolio.drop_duplicates(keep='last')
# Naming the column of log returns 'Log'
Portfolio.columns= ['Log']
# Cumsum() dos log returns para obter o preço do Portfolio
Portfolio['Price'] = 100*np.exp(np.nan_to_num(Portfolio['Log'].cumsum()))
# Round Portfolio to 2 decimals and eliminate returns
Portfolio = pd.DataFrame(round(Portfolio['Price'], 2))
# Naming the column of Portfolio as 'Portfolio'
Portfolio.columns= ['Portfolio']
# Delete repeated days
Portfolio = Portfolio.loc[~Portfolio.index.duplicated(keep='first')]
return Portfolio
# Multi_period_return (in CAGR)
def multi_period_return(df, years = 1, days=252):
shifted = df.shift(days * years)
One_year = (((1 + (df - shifted) / shifted) ** (1 / years))-1) * 100
return One_year
def compute_drawdowns_i(dataframe):
'''
Function to compute drawdowns based on
the inicial value of a timeseries
given a dataframe of prices
'''
return (dataframe / 100 -1) * 100
def print_title(string):
display(Markdown('**' + string + '**'))
def all_percent(df):
for column in df.columns:
df[column] = df[column].apply( lambda x : str(x) + '%')
return df
def preview(df):
return pd.concat([df.head(3), df.tail(3)])
def normalize(df):
df = df.dropna()
return (df / df.iloc[0]) * 100
dimensions=(990, 500)
colorz = ['royalblue', 'orange', 'dimgrey', 'darkorchid']
class color:
PURPLE = '\033[95m'
CYAN = '\033[96m'
DARKCYAN = '\033[36m'
BLUE = '\033[94m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
END = '\033[0m'
### print(color.BOLD + 'Hello World !' + color.END)
##################################################
### Begin of compute_drawdowns_table function ####
##################################################
### Função auxiliar 1
def compute_time_period(timestamp_1, timestamp_2):
year = timestamp_1.year - timestamp_2.year
month = timestamp_1.month - timestamp_2.month
day = timestamp_1.day - timestamp_2.day
if month < 0:
year = year - 1
month = 12 + month
if day == 0:
day = - day
if day < 0:
month = month - 1
if timestamp_1.month not in [1, 3, 5, 7, 8, 10, 12]:
day = 31 + day
else:
day = 30 + day
# Returns datetime object in years, month, days
return(str(year) + ' Years, ' + str(month) + ' Months, ' + str(day) + ' Days')
### Função auxiliar 2
def compute_drawdowns_periods(df):
# Input: df of max points in drawdowns (where dd == 0)
drawdown_periods = list()
for i in range(0, len(df.index)):
drawdown_periods.append(compute_time_period(df.index[i], df.index[i - 1]))
drawdown_periods = pd.DataFrame(drawdown_periods)
return (drawdown_periods)
### Função auxiliar 3
def compute_max_drawdown_in_period(prices, timestamp_1, timestamp_2):
df = prices[timestamp_1:timestamp_2]
max_dd = compute_max_DD(df)
return max_dd
### Função auxiliar 4
def compute_drawdowns_min(df, prices):
# Input: df of max points in drawdowns (where dd == 0)
drawdowns_min = list()
for i in range(0, len(df.index) - 1):
drawdowns_min.append(compute_max_drawdown_in_period(prices, df.index[i], df.index[i + 1]))
drawdowns_min = pd.DataFrame(drawdowns_min)
return(drawdowns_min)
### Função principal
def compute_drawdowns_table(prices, number=5):
# input: df of prices
dd = compute_drawdowns(prices)
max_points = dd[dd == 0].dropna()
data = [0.0]
# Create the pandas DataFrame
new_data = pd.DataFrame(data, columns = ['New_data'])
new_data['Date'] = prices.index.max()
new_data.set_index('Date', inplace=True)
max_points = max_points.loc[~max_points.index.duplicated(keep='first')]
max_points = pd.DataFrame(pd.concat([max_points, new_data], axis=1).iloc[:, 0])
dp = compute_drawdowns_periods(max_points)
dp.set_index(max_points.index, inplace=True)
df = pd.concat([max_points, dp], axis=1)
df.index.name = 'Date'
df.reset_index(inplace=True)
df['End'] = df['Date'].shift(-1)
df[0] = df[0].shift(-1)
df['values'] = round(compute_drawdowns_min(max_points, prices), 2)
df = df.sort_values(by='values')
df['Number'] = range(1, len(df) + 1)
df.reset_index(inplace=True)
df.columns = ['index', 'Begin', 'point', 'Length', 'End', 'Depth', 'Number']
df = df[['Begin', 'End', 'Depth', 'Length']].head(number)
df.iloc[:, 2] = df.iloc[:, 2].apply( lambda x : str(x) + '%')
df.set_index(np.arange(1, number + 1), inplace=True)
df['End'] = df['End'].astype(str)
df['Begin'] = df['Begin'].astype(str)
for i in range(0, len(df['End'])):
if df['End'].iloc[i] == str(prices.iloc[-1].name)[0:10]:
df['End'].iloc[i] = str('N/A')
return(df)
################################################
### End of compute_drawdowns_table function ####
################################################
def compute_r2(x, y, k=1):
xpoly = np.column_stack([x**i for i in range(k+1)])
return sm.OLS(y, xpoly).fit().rsquared
def compute_r2_table(df, benchmark):
# df of prices
lista = []
for i in np.arange(0, len(df.columns)):
lista.append(compute_r2(benchmark, df.iloc[: , i]))
Dataframe = pd.DataFrame(lista)
Dataframe.index = df.columns
Dataframe.columns = [benchmark.name]
return(round(Dataframe.transpose(), 3))
colors = ['royalblue', # 1 - royalblue
'dimgrey', # 2 - dimgrey
'rgb(255, 153, 51)', # 3 - orange
'indigo', # 4 - Indigo
'rgb(219, 64, 82)', # 5 - Red
'rgb(0, 128, 128)', # 6 - Teal
'#191970', # 7 - Navy
'rgb(128, 128, 0)', # 8 - Olive
'#00BFFF', # 9 - Water Blue
'rgb(128, 177, 211)'] # 10 - Blueish
def compute_costs(DataFrame, percentage, sessions_per_year=365, Nome='Price'):
DataFrame = pd.DataFrame(DataFrame.copy())
DataFrame['Custos'] = (percentage/sessions_per_year) / 100
DataFrame['Custos_shifted'] = DataFrame['Custos'].shift(1)
DataFrame['Custos_acumulados'] = DataFrame['Custos_shifted'].cumsum()
DataFrame[Nome] = DataFrame.iloc[ : ,0] * (1-DataFrame['Custos_acumulados'])
DataFrame = DataFrame[[Nome]]
DataFrame = DataFrame.fillna(100)
return DataFrame
def compute_ms_performance_table(DataFrame, freq='days'):
nr_of_days = int(str(DataFrame.index[-1] - DataFrame.index[0])[0:4])
if nr_of_days < 365:
df = compute_performance_table(DataFrame, freq=freq)
df.index = ['S.I.']
return df
elif nr_of_days >= 365 and nr_of_days < 365*3:
df0 = compute_performance_table(DataFrame)
df1 = compute_performance_table(filter_by_date(DataFrame, years=1), freq=freq)
df = pd.concat([df0, df1])
df.index = ['S.I.', '1 Year']
return df
elif nr_of_days >= 365*3 and nr_of_days < 365*5:
df0 = compute_performance_table(DataFrame)
df1 = compute_performance_table(filter_by_date(DataFrame, years=1), freq=freq)
df3 = compute_performance_table(filter_by_date(DataFrame, years=3), freq=freq)
df = pd.concat([df0, df1, df3])
df.index = ['S.I.', '1 Year', '3 Years']
return df
elif nr_of_days >= 365*5 and nr_of_days < 365*10:
df0 = compute_performance_table(DataFrame)
df1 = compute_performance_table(filter_by_date(DataFrame, years=1), freq=freq)
df3 = compute_performance_table(filter_by_date(DataFrame, years=3), freq=freq)
df5 = compute_performance_table(filter_by_date(DataFrame, years=5), freq=freq)
df = pd.concat([df0, df1, df3, df5])
df.index = ['S.I.', '1 Year', '3 Years', '5 Years']
return df
elif nr_of_days >= 365*10 and nr_of_days < 365*15:
df0 = compute_performance_table(DataFrame, freq=freq)
df1 = compute_performance_table(filter_by_date(DataFrame, years=1), freq=freq)
df3 = compute_performance_table(filter_by_date(DataFrame, years=3), freq=freq)
df5 = compute_performance_table(filter_by_date(DataFrame, years=5), freq=freq)
df10 = compute_performance_table(filter_by_date(DataFrame, years=10), freq=freq)
df = pd.concat([df0, df1, df3, df5, df10])
df.index = ['S.I.', '1 Year', '3 Years', '5 Years', '10 Years']
return df
elif nr_of_days >= 365*15 and nr_of_days < 365*20:
df0 = compute_performance_table(DataFrame, freq=freq)
df1 = compute_performance_table(filter_by_date(DataFrame, years=1), freq=freq)
df3 = compute_performance_table(filter_by_date(DataFrame, years=3), freq=freq)
df5 = compute_performance_table(filter_by_date(DataFrame, years=5), freq=freq)
df10 = compute_performance_table(filter_by_date(DataFrame, years=10), freq=freq)
df15 = compute_performance_table(filter_by_date(DataFrame, years=15), freq=freq)
df = pd.concat([df0, df1, df3, df5, df10, df15])
df.index = ['S.I.', '1 Year', '3 Years', '5 Years', '10 Years', '15 Years']
return df
def compute_log_returns(prices):
"""
Compute log returns for each ticker.
INPUT
----------
prices
OUTPUT
-------
log_returns
"""
return np.log(prices) - np.log(prices.shift())
def merge_time_series(df_1, df_2, how='left'):
df = df_1.merge(df_2, how=how, left_index=True, right_index=True)
return df
def compute_rolling_cagr(dataframe, years):
rolling_result = []
number = len(dataframe)
for i in np.arange(1, number + 1):
df = dataframe.iloc[:i]
df = filter_by_years(df, years=years)
result = (((df.iloc[-1] / df.iloc[0]) ** (1/years) - 1))
rolling_result.append(result[0])
final_df = pd.DataFrame(data = rolling_result, index = dataframe.index[0:number], columns = ['Ret'])
final_df = final_df.loc[dataframe.index[0] + pd.DateOffset(years=years):]
return final_df
def filter_by_years(dataframe, years=0):
last_date = dataframe.tail(1).index
year_nr = last_date.year.values[0]
month_nr = last_date.month.values[0]
day_nr = last_date.day.values[0]
if month_nr == 2 and day_nr == 29 and years % 4 != 0:
new_date = str(year_nr - years) + '-' + str(month_nr) + '-' + str(day_nr-1)
else:
new_date = str(year_nr - years) + '-' + str(month_nr) + '-' + str(day_nr)
df = dataframe.loc[new_date:]
dataframe = pd.concat([dataframe.loc[:new_date].tail(1), dataframe.loc[new_date:]])
# Delete repeated days
dataframe = dataframe.loc[~dataframe.index.duplicated(keep='first')]
return dataframe
a = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print('A última vez que este script foi executado foi em:', a)
A última vez que este script foi executado foi em: 2021-04-23 15:36:22
start = time.time()
Quotes = pd.read_csv('D:/GDrive/_GitHub/Backtester/Data/Cotacoes_diarias_all.csv')
end = time.time()
print(end - start)
0.5497429370880127
ISINs = ['LU0114721508', 'LU0245181838', 'LU1670724373', 'IE00B11XZ103']
Nomes=['FCI', 'GS_GSC', 'MG_OI', 'PimcoGB']
Quotes = pd.read_csv('D:/GDrive/_GitHub/Backtester/Data/Cotacoes_diarias_all.csv', index_col='Date', parse_dates=True)[ISINs].dropna()
Quotes.columns=Nomes
portfolio_def = compute_portfolio(quotes=Quotes, weights=[0.15, 0.15, 0.15, 0.55], Nomes=Nomes)
portfolio_ew = compute_portfolio(quotes=Quotes, weights=[0.25, 0.25, 0.25, 0.25], Nomes=Nomes)
portfolio_agr = compute_portfolio(quotes=Quotes, weights=[0.40, 0.40, 0.10, 0.10], Nomes=Nomes)
fundos = portfolio_def.merge(portfolio_ew, how='left', left_index=True, right_index=True)
fundos = fundos.merge(portfolio_agr, how='left', left_index=True, right_index=True)
fundos_nomes = ['4Fundos DEF', '4Fundos EW', '4Fundos AGR']
fundos.columns = fundos_nomes
Em baixo vemos a matriz de correlaçaõ dos retornos dos fundos. Vemos facilmente "blocos" com uma correlação elevada entre os fundos acionistas, que por sua vez não têm qualquer correlação com os fundos obrigacionistas. O oposto também se pode ver, embra a correlação entre o Pimco GB e o MG_OI não seja assim tão alta, fruto da maior flexibilidade que o gestor do MG_OI tem em seu dispôr.
A existência de fundos descorrelacionados ou com baixa correlação é provavelmente a melhor situação realista, e contribui bastante para a construção de uma boa carteira.
# Correlation Matrix
Corr_matrix = Quotes.pct_change().corr()
# Plotting the correlation matrix
fig, ax = plt.subplots()
fig.set_size_inches(5.5, 4)
sns.heatmap(Corr_matrix, annot = True, cmap = "coolwarm", linewidths=.2, vmin = -1)
plt.yticks(rotation=360)
plt.title('Matrix de correlação')
plt.show()
#### Normalization to 100 ####
fundos_norm = round((fundos.div(fundos.iloc[0]).mul(100)), 2)
#### Returns ####
P_returns = fundos_norm.pct_change()
Antes de chegarmos à tabelas e ao pormenor de análise de risco, sou apologista que um gráfico vale por 1000 palavras e apresento o gráfico de evolução da carteira.
Penso que o que está imediatamente abaixo é de simples compreensão. É aquilo a que estamos habituamos a ver mais. A evolução dos investimentos em cada fundo. Neste caso estão normalizados a 100. Esta é a evolução ao longo dos anos de cada 100 euros investidos.
Dica: A interactividade dos gráficos da plotly permite:
. Zoom (mantenham o clique e arrastem o cursor em simultâneo, em cima do gráfico);
. Tem uma lista de opções que aparece se forem com o cursor ao canto superior direito;
. Façam duplo clique no gráfico ou seleccionem a opção Autoscale/Reset Axes para voltar ao normal;
. Alterem a opção de 'Compare data on hoover' para 'Show closest data on hoover' se vos for mais conveniente;
. Se clicarem num nome na legenda esse fundo desaparece (clicando de novo ele volta a apetecer).
fundos_norm.iplot(kind='scatter',yTitle='Valor por cada 100€ investidos',
title='Performance das carteiras', colors=['royalblue', 'orange', 'dimgray'])
O segundo gráfico é um gráfico em escala logaritmica. Porque um gráfico de escala logaritmica? Um gráfico de escala logaritmica é um gráfico onde o espaço visual é medido em percentagem, e não em euros. Ou seja, o problema dos gráficos de longo prazo é que à medida que a carteira sobe uma variação de 5 euros, deixa de ser uma variação de 5% mas de apenas 2%. Isso faz com que as variações mais antigas pareçam pequenas e amplia as mais recentes.
Para colmatar isso o gráfico com escala logaritmica mostra o mesmo espaço entre 100 e 105 como mostra entre 200 e 210, pois afinal são ambas variações de 5%. Isso acaba por torar um gráfico de longo prazo mais realista. Apercebem-se como a crise de 2008 parece mais volátil no gráficode escala logaritmica e as recentes subidas mais pequenas? Esse é o objectivo e uma consequência de medição das variações em termos relativos (percentuais) em vez de absolutos (por montante de euros).
fundos_norm.iplot(kind='scatter',yTitle='Valor por cada 100€ investidos',
title='Performance das carteiras (versão log)', logy=True, colors=['royalblue', 'orange', 'dimgray'])
Este último gráfico pode ser incrivelmente simples, para quem já o viu, ou complexo para quem não sabe o conceito de drawdown. Drawdown é o conceito ficenanceiro que resumidamente pode-se referir como "quedas a partir do máximo". Este é um conceito muito importante e mostra os valores percentuais que a carteira está do seus máximos históricos. Responde à pergunta, se eu tivesse comprado no pico quanto estaria agora a perder?
Esclareço, contudo, que há diferentes picos, como seria de esperar numa carteira que se encontre em ascensão. Sempre que o drawdown atinge 0 é sinal que fez um novo pico (ou igualou o anterior), pois, por definição, uma carteira nunca poderá estar acima do seu máximo histórico.
#### Computing Drawdowns ####
DD = round(compute_drawdowns(fundos_norm), 2)
DD.iplot(kind='scatter', title='Drawdown dos portfolios', yTitle='Percentagem', colors=['royalblue', 'orange', 'dimgray'])
DD.tail(1)
4Fundos DEF | 4Fundos EW | 4Fundos AGR | |
---|---|---|---|
2021-04-19 | 0.00 | 0.00 | 0.00 |
#### Returns ####
returns = (fundos_norm / fundos_norm.shift(1)) - 1
# Resampling to yearly (business year)
yearly_quotes = fundos_norm.resample('BA').last()
# Adding first quote (only if start is in the middle of the year)
yearly_quotes = pd.concat([fundos_norm.iloc[:1], yearly_quotes])
Esta é a tabela de performance desde o início do backtest. O CAGR diz-nos a rentabilidade anualizada ao longo do período e o StdDev diz-nos a volatilidade anualizada.
O retorno foi maior quanto maior foi o risco da carteira. Algo que poderá nem sempre acontecer, especialmente em espaços curtos de tempo. Mas neste caso, em em linha com as teorias financeiras temos um retorno maior pelo risco assumido.
Algo que é independente do período é as carteiras com mais risco apresentarem, de facto um maior desvio padrão. Ou seja, quanto mais agressiva a carteira maior o seu risco, naturalmente. Temos é uma visão mais quantitativa desse risco. O desvio padrão não é a única forma de medir risco mas é bastante comum e importante.
Para comparação entre as carteiras temos também o rácio de Sharpe. Neste caso um simples rácio CAGR/StdDev. Quanto mais alto melhor, pois ele mede a rentabilidade por cada unidade de risco. Quando falamos de rentabilidades relativamente baixas um pequeno aumento de risco aaba por se transformar num equivalente bom aumento de rentabilidade. Contudo, em rentabilidades mais elevadas para aumentar um pouco mais a rentabilidade teremos de aumentar significativamente o risco. Daí as carteiras com maior risco normalmente terem um menor Sharpe. O aumento de risco para ter mais um pouco de rentabilidade tem de ser significativo.
Outra forma de medir o risco pode ser pela maior queda que a carteira já teve. Este acaba por ser uma medida de risco a que eu pessoalmente acabo por dar maior importância. É efectivamente o que nos cria stress e pode levar à vontade de vender a carteira nos momentos mais inoportunos. Termos uma boa ideia de quanto aguentamos em drawdown máximo é algo importante para conseguirmos ter o conforto de manter os investimentos a médio/longo prazo.
compute_performance_table(fundos)
CAGR | StdDev | Sharpe | Max DD | MAR | |
---|---|---|---|---|---|
4Fundos DEF | 5.83% | 4.16% | 1.40 | -20.91% | 0.28 |
4Fundos EW | 7.08% | 6.47% | 1.09 | -30.56% | 0.23 |
4Fundos AGR | 8.23% | 10.17% | 0.81 | -43.91% | 0.19 |
Podemos ver, em baixo, quadros com os respectivos retornos anuais e mensais de cada carteira. É possível quantificar a queda em 2008, e mais especificamente o período de falência da lehman brothers (olhando para o quadro mensal) , onde até a carteira defensiva teve uma queda significativa. De igual forma conseguimos ver a rápida e forte recuperação e consequente continuação da subida desde a crise.
# Returns
yearly_returns = ((yearly_quotes / yearly_quotes.shift(1)) - 1) * 100
yearly_returns = yearly_returns.set_index([list(range(2006, 2022))]).drop(2006)
#### Inverter o sentido das rows no dataframe ####
yearly_returns = yearly_returns.transpose()
# Yearly returns heatmap
fig, ax = plt.subplots()
fig.set_size_inches(17, 3.5) # 68%
heatmap = sns.heatmap(yearly_returns, annot=True, cmap="RdYlGn", linewidths=.2, fmt=".2f", cbar=False, center=0)
for t in heatmap.texts: t.set_text(t.get_text() + "%")
plt.title('Retornos Anuais')
plt.yticks(rotation=360)
plt.show()
plot(fundos_norm['4Fundos DEF'], title= 'Retornos mensais para 4Fundos DEF', figsize=(16,12))
plot(fundos_norm['4Fundos EW'], title= 'Retornos mensais para 4Fundos EW', figsize=(16,12))
plot(fundos_norm['4Fundos AGR'], title= 'Retornos mensais para 4Fundos AGR', figsize=(16,12))
O sumário de estatísticas para os retornos mensais, assim como respectivos gráficos de boxplot e swarmplot, mostra o que poderemos esperar de uma variação mensal. Podemos ver não só a variação mensal máxima como a mínima de cada carteira, assim como a variação mediana.
# Turning daily quotes into monthly
fundos_norm_M = fundos_norm.resample('BM').last()
# Monthly returns
fundos_norm_ret_M = fundos_norm_M.pct_change() * 100
fundos_norm_ret_M.describe().iloc[[1, 3, 4, 5, 6, 7],:]
4Fundos DEF | 4Fundos EW | 4Fundos AGR | |
---|---|---|---|
mean | 0.49 | 0.60 | 0.72 |
min | -7.10 | -9.86 | -12.27 |
25% | -0.26 | -0.48 | -0.79 |
50% | 0.64 | 0.84 | 1.04 |
75% | 1.45 | 1.99 | 2.68 |
max | 5.48 | 7.92 | 10.95 |
No boxplot podemos ver bem a parte central da vela (que tem 50% das variações) a ter de aumentar consoante o risco da carteira. Enquanto a carteira defensiva tem 50% das variações mensais compreendidas entre -0.23 e 1.47 a agressiva, por força da sua maior volatilidade, já tem de aumentar o intervalo, indo de -0.78 até 2.62.
As barras do boxplot vão até onde faz sentido para albergar praticamente todas as variações, pois apenas excluir os valores extremos (outliers). Isso é mais visível no swarmplot.
my_pal = ["royalblue", "orange", "dimgrey"]
plt.figure(figsize=(7, 6), dpi=80)
my_pal = ["royalblue", "orange", "dimgrey"]
ax = sns.boxplot(data=fundos_norm_ret_M, orient="v", linewidth=1, width=0.25, fliersize=3, palette=my_pal, whis=1.5)
ax.set_title("Boxplot das rentabilidades mensais (com outliers)")
ax.set_xlabel('')
ax.set_ylabel('Percentagem da variação')
plt.show()
A leitura do swarmplot é semelhante à do boxplot mas com a pequena distinção que conseguimos visualizar a quantidade de pontos (cada uma a representar uma variação mensal). Como esperado as observações encontram-se concentradas perto da mediana e ao longo do já falado corpo principal da vela do boxplot. Neste gráfico são bastante visíveis os outliers, e como estão distanciados do comportamento normal das carteiras.
ax = sns.swarmplot(data=fundos_norm_ret_M, orient='v', linewidth=1, palette=my_pal)
ax.set_title("Swarmplot das rentabilidades mensais")
ax.set_xlabel('')
ax.set_ylabel('Percentagem da variação')
plt.show()
As tabelas de drawdowns são bastante úteis, pois podemos visualizar as quedas em toda a sua dimensão, quer em termos de profundidade como tempo. Os quadros incluem várias métricas, mas alerto que os periodos estão em sessões, não em dias. Cada ano tem cerca de 260 sessões, no nosso caso, daí o periodo de 2007-06-04 a 2009-03-09 (mais de 2 anos) só incluir "613" sessões e não mais de 800 como seria de esperar se fossem em dias.
Podemos aqui rápidamente quantificar o gráfico dos drawdown acima e ver a profundidade e tempo das 5 maiores quedas de cada carteira. Naturalmente quanto mais risco tem a carteira não só tem quedas mais profundas como demora mais tempo a voltar a atingir novos máximos históricos.
compute_drawdowns_table(fundos[['4Fundos DEF']])
Begin | End | Depth | Length | |
---|---|---|---|---|
1 | 2007-06-03 | 2009-10-07 | -20.91% | 2 Years, 4 Months, 4 Days |
2 | 2020-02-19 | 2020-10-12 | -15.85% | 0 Years, 7 Months, 23 Days |
3 | 2015-04-15 | 2016-08-05 | -8.94% | 1 Years, 3 Months, 20 Days |
4 | 2018-08-12 | 2019-03-29 | -6.84% | 0 Years, 7 Months, 17 Days |
5 | 2013-05-19 | 2013-11-14 | -6.03% | 0 Years, 5 Months, 26 Days |
compute_drawdowns_table(fundos[['4Fundos EW']])
Begin | End | Depth | Length | |
---|---|---|---|---|
1 | 2007-06-03 | 2010-03-04 | -30.56% | 2 Years, 9 Months, 1 Days |
2 | 2020-02-19 | 2020-11-16 | -22.49% | 0 Years, 8 Months, 28 Days |
3 | 2015-04-13 | 2016-11-24 | -12.72% | 1 Years, 7 Months, 11 Days |
4 | 2018-06-17 | 2019-04-03 | -10.39% | 0 Years, 9 Months, 17 Days |
5 | 2011-07-11 | 2012-01-03 | -7.91% | 0 Years, 5 Months, 22 Days |
compute_drawdowns_table(fundos[['4Fundos AGR']])
Begin | End | Depth | Length | |
---|---|---|---|---|
1 | 2007-06-03 | 2010-11-26 | -43.91% | 3 Years, 5 Months, 23 Days |
2 | 2020-02-19 | 2020-11-24 | -29.6% | 0 Years, 9 Months, 5 Days |
3 | 2015-04-13 | 2016-12-08 | -17.5% | 1 Years, 7 Months, 25 Days |
4 | 2018-06-14 | 2019-04-12 | -15.18% | 0 Years, 9 Months, 29 Days |
5 | 2011-01-11 | 2012-01-17 | -13.4% | 1 Years, 0 Months, 6 Days |
Os 3 gráficos abaixo mostram, para cada carteira, quanto tempo ela passou abaixo de determinado drawdown. Como já visto acima a carteira defensiva tem um drawdown menor que a moderada ou agressiva, sendo isso visivel pelo valor mais à direita na escala do x. Da mesma forma, é de esperar que tenha passado menos tempo abaixo de um determinado drawdown, pois quanto mais agressiva uma carteira maiores as quedas mas também mais tempo se passa em valores longe dos máximos, algo é fruto dessas mesmas quedas.
Escolhendo arbitrariamente um valor de 5% podemos ver que a carteira defensiva passou 16.76% do tempo em que estava a 5% ou mais do seu máximo histórico. Por seu lado, e em linha com as nossas espectativas a carteira moderada passou mais tempo com um drawdown igual ou superior a 5%, neste caso 31.82%. Por último a carteira agressiva passou 43.26% do seu tempo em que se encontrava pelo menos a 5% do seu máximo histórico.
O tempo, que se passa longe dos máximos históricos é psicológicamente desgastantante, e uma clara alusão à capacidade de cada um de nós de aguentar perdas, quer em profundidade como em dimensão.
À pessoa mais curiosa deixo a tarefa de fazer o oposto. Será natural a carteira com menos risco passar mais tempo com uma queda inferior 1%. Podem por vocês mesmos verificar isso, que é exactamente o que acontece.
layout = go.Layout(
title='Histograma dos drawdowns da 4Fundos DEF',
plot_bgcolor='#f5f5f5',
paper_bgcolor='#f5f5f5',
xaxis=dict(
title='Drawdown',
showgrid=True,
),
yaxis=dict(
title='Percentagem de tempo abaixo de um dado drawdown'
))
data = [go.Histogram(x=DD['4Fundos DEF'], histnorm='percent', marker=dict(colorscale='RdBu', reversescale=False, cmax=0, cmin=-23, color=np.arange(-43, 0),
line=dict(color='white', width=0.2)), opacity=0.75, cumulative=dict(enabled=True))]
fig = go.Figure(data=data, layout=layout)
iplot(fig)
layout = go.Layout(
title='Histograma dos drawdowns da 4Fundos EW',
plot_bgcolor='#f5f5f5',
paper_bgcolor='#f5f5f5',
xaxis=dict(
title='Drawdown',
showgrid=True,
),
yaxis=dict(
title='Percentagem de tempo abaixo de um dado drawdown'
))
data = [go.Histogram(x=DD['4Fundos EW'], histnorm='percent', marker=dict(colorscale='RdBu', reversescale=False, cmax=0, cmin=-24, color=np.arange(-62, 0),
line=dict(color='white', width=0.2)), opacity=0.75, cumulative=dict(enabled=True))]
fig = go.Figure(data=data, layout=layout)
iplot(fig)
layout = go.Layout(
title='Histograma dos drawdowns da 4Fundos AGR',
plot_bgcolor='#f5f5f5',
paper_bgcolor='#f5f5f5',
xaxis=dict(
title='Drawdown',
showgrid=True,
),
yaxis=dict(
title='Percentagem de tempo abaixo de um dado drawdown'
))
data = [go.Histogram(x=DD['4Fundos AGR'], histnorm='percent', marker=dict(colorscale='RdBu', reversescale=False, cmax=0, cmin=-24, color=np.arange(-45, 0),
line=dict(color='white', width=0.2)), opacity=0.75, cumulative=dict(enabled=True))]
fig = go.Figure(data=data, layout=layout)
iplot(fig)
Em baixo podemos ver várias tabelas de performance para periodos compreendidos entre 1, 3, 5, e 10 anos. Podemos assim ver qual a rentabilidade anualizada em diferentes periodos, assim como o desvio padrão e a maior queda nesse periodo. O sharpe, como já referido acima, é um bom comparador para ver a qualidade das carteiras.
Em baixo de cada tabela podemos ver os gráficos da evolução da carteira, para termos uma noção visual da evolução no periodo em causa.
# One year dataframe for portfolios
one_year_df = filter_by_date(fundos, years=1)
compute_performance_table(one_year_df, years=1)
CAGR | StdDev | Sharpe | Max DD | MAR | |
---|---|---|---|---|---|
4Fundos DEF | 15.23% | 4.04% | 3.77 | -2.56 | 5.94 |
4Fundos EW | 24.09% | 6.56% | 3.67 | -3.23 | 7.46 |
4Fundos AGR | 35.68% | 10.18% | 3.50 | -4.96 | 7.20 |
# Normalization to 100
one_year_df_norm = round((one_year_df.div(one_year_df.iloc[0]).mul(100)), 2)
# Plotting
one_year_df_norm.iplot(kind='scatter',yTitle='Valor por cada 100€ investidos', title='Gráfico de performance das carteiras a 1 ano', colors=['royalblue', 'orange', 'dimgray'])
# Three years dataframe for portfolios
three_years_df = filter_by_date(fundos, years=3)
compute_performance_table(three_years_df, years=3)
CAGR | StdDev | Sharpe | Max DD | MAR | |
---|---|---|---|---|---|
4Fundos DEF | 5.58% | 4.77% | 1.17 | -15.85 | 0.35 |
4Fundos EW | 7.98% | 7.54% | 1.06 | -22.49 | 0.36 |
4Fundos AGR | 11.54% | 11.61% | 0.99 | -29.60 | 0.39 |
# Normalization to 100
three_years_df_norm = round((three_years_df.div(three_years_df.iloc[0]).mul(100)), 2)
# Plotting
three_years_df_norm.iplot(kind='scatter', yTitle='Valor por cada 100€ investidos',
title='Gráfico de performance das carteiras a 3 anos',
colors=['royalblue', 'orange', 'dimgray'])
# Five years dataframe for portfolios
five_years_df = filter_by_date(fundos, years=5 , previous_row=True)
# Five years Performance table
compute_performance_table(five_years_df, years=5)
CAGR | StdDev | Sharpe | Max DD | MAR | |
---|---|---|---|---|---|
4Fundos DEF | 4.98% | 4.12% | 1.21 | -15.85 | 0.31 |
4Fundos EW | 7.30% | 6.52% | 1.12 | -22.49 | 0.32 |
4Fundos AGR | 10.29% | 10.04% | 1.02 | -29.60 | 0.35 |
# Normalization to 100
five_years_df_norm = round((five_years_df.div(five_years_df.iloc[0]).mul(100)), 2)
# Plotting
five_years_df_norm.iplot(kind='scatter',yTitle='Valor por cada 100€ investidos',
title='Gráfico de performance das carteiras a 5 anos', colors=['royalblue', 'orange', 'dimgray'])
# Ten years dataframe for portfolios
ten_years_df = filter_by_date(fundos, years=10, previous_row=True)
# Ten years Performance table
compute_performance_table(ten_years_df, years=10)
CAGR | StdDev | Sharpe | Max DD | MAR | |
---|---|---|---|---|---|
4Fundos DEF | 6.31% | 3.98% | 1.59 | -15.85 | 0.40 |
4Fundos EW | 8.39% | 6.20% | 1.35 | -22.49 | 0.37 |
4Fundos AGR | 11.11% | 9.57% | 1.16 | -29.60 | 0.38 |
ten_years_df = filter_by_date(fundos_norm, years=10)
# Normalization to 100
ten_years_df_norm = round((ten_years_df.div(ten_years_df.iloc[0]).mul(100)), 2)
# Plotting
ten_years_df_norm.iplot(kind='scatter',yTitle='Valor por cada 100€ investidos', title='Gráfico de performance das carteiras a 10 anos', colors=my_pal)
rr_def_1 = compute_rolling_cagr(fundos[['4Fundos DEF']], years=1)
rr_def_3 = compute_rolling_cagr(fundos[['4Fundos DEF']], years=3)
rr_def_5 = compute_rolling_cagr(fundos[['4Fundos DEF']], years=5)
rr_def_10 = compute_rolling_cagr(fundos[['4Fundos DEF']], years=10)
rr_ew_1 = compute_rolling_cagr(fundos[['4Fundos EW']], years=1)
rr_ew_3 = compute_rolling_cagr(fundos[['4Fundos EW']], years=3)
rr_ew_5 = compute_rolling_cagr(fundos[['4Fundos EW']], years=5)
rr_ew_10 = compute_rolling_cagr(fundos[['4Fundos EW']], years=10)
rr_agr_1 = compute_rolling_cagr(fundos[['4Fundos AGR']], years=1)
rr_agr_3 = compute_rolling_cagr(fundos[['4Fundos AGR']], years=3)
rr_agr_5 = compute_rolling_cagr(fundos[['4Fundos AGR']], years=5)
rr_agr_10 = compute_rolling_cagr(fundos[['4Fundos AGR']], years=10)
# compute_rollling
one_year_rolling = merge_time_series(rr_def_1, rr_ew_1, how='inner')
one_year_rolling = merge_time_series(one_year_rolling, rr_agr_1, how='inner') * 100
one_year_rolling.columns = fundos_nomes
three_years_rolling = merge_time_series(rr_def_3, rr_ew_3, how='inner')
three_years_rolling = merge_time_series(three_years_rolling, rr_agr_3, how='inner') * 100
three_years_rolling.columns = fundos_nomes
five_years_rolling = merge_time_series(rr_def_5, rr_ew_5, how='inner')
five_years_rolling = merge_time_series(five_years_rolling, rr_agr_5, how='inner') * 100
five_years_rolling.columns = fundos_nomes
ten_years_rolling = merge_time_series(rr_def_10, rr_ew_10, how='inner')
ten_years_rolling = merge_time_series(ten_years_rolling, rr_agr_10, how='inner') * 100
ten_years_rolling.columns = fundos_nomes
Os retornos rolantes são gráficos muito interessantes, pois mostram-nos, de forma continua, a rentabilidade para determinado periodo que cada carteira teve. Podemos ver, por exemplo, que a carteira agressiva chegou a ter uma rentabilidade de 56% quando foi da recuperação da cride 2008. Por outro lado, no mesmo periodo a carteira defensiva "só" subiu 36%, o que é natural dada a estrutura defensiva da mesma.
round(one_year_rolling, 2).iplot(kind='scatter', title='Retornos rolantes a 1 ano', yTitle='Percentagem da variação dos retornos a 1 ano', colors=['royalblue', 'orange', 'dimgray'])
Para reforçar a ideia que tentei mostrar acima, de que quanto maior o risco maior a variabilidade dos retornos temos também este boxplot. O boxplot dá uma visão diferente mas em linha com essa ideia.
Facilmente podemos ver que a barra/boxplot referente à carteira defensiva teve uma menor amplite do que a referente à carteira EW. Os pontos positivos e negativos das carteiras com mais risco são significamente mais extremos do que os da carteira defensiva.
my_pal = ["royalblue", "orange", "dimgrey", 'indigo']
plt.figure(figsize=(7, 6), dpi=80)
ax = sns.boxplot(data=one_year_rolling, orient="v", linewidth=1, width=0.25, fliersize=3, palette=my_pal, whis=1.5)
ax.set_title("Boxplot dos retornos rolantes a 1 ano (com outliers)")
ax.set_xlabel('')
ax.set_ylabel('Percentagem da variação dos retornos a 1 ano')
plt.show()
Quando começamos a analisar retornos rolantes a periodos mais longos, como por exemplo 3 anos podemos ver que o primeiro valor da carteira defensiva foi de 4.05% em 22 de Abril de 2010. Isso significa que quem comprou em 22 de Abril de 2007 (3 anos antes e mesmo antes da crise começar) teve uma rentabilidade anualizada de 4.05%. Por outro lado quem tinha a carteira agressiva nesse periodo teve uma rentabilidade anualizada de -0.64, tendo perdido dinheiro, mesmo aguentando os 3 anos com a carteira.
Podemos ver também que históricamente, quem teve a carteira defensiva ou a moderada durante 3 anos apresentou sempre lucros. Lembremos, contudo, que retornos passados não são garantia de retornos futuros.
round(three_years_rolling, 2).iplot(kind='scatter', title='Retornos rolantes a 3 anos', yTitle='Percentagem da variação dos retornos a 3 anos', colors=my_pal)
Da mesma forma do que para o periodo a 1 ano, podemos ver que 3 anos a variabilidade dos retornos são maiores quanto mais arriscadas as carteiras. Podemos aqui rápidamente confirmar que nem a defensiva nem a carteira EW tiveram nenhuns periodos negativos a 3 anos (o que não quer dizer que não podem acontecer, só diz que nunca aconteceu).
my_pal = ["royalblue", "orange", "dimgrey", 'indigo']
plt.figure(figsize=(7, 6), dpi=80)
ax = sns.boxplot(data=three_years_rolling, orient="v", linewidth=1, width=0.25, fliersize=3, palette=my_pal, whis=1.5)
ax.set_title("Boxplot dos retornos rolantes a 3 anos (com outliers)")
ax.set_xlabel('')
ax.set_ylabel('Percentagem da variação dos retornos a 3 anos')
plt.show()
Em baixo podemos ver os gráficos de rentabilidade de quem tinha investido há 5 anos, para cada periodo após o quinto aniversário do backtest.
round(five_years_rolling, 2).iplot(kind='scatter', title='Retornos rolantes a 5 anos', yTitle='Percentagem', colors=['royalblue', 'orange', 'dimgray'])
Como para os períodos anteriores mostro aqui o boxplot dos retornos a 5 anos. Podemos facilmente ver que nem a carteira agressiva teve periodos negativos a 5 anos. Mesmo com uma carteira agressiva, numa situação tão extrema como a crise de 2008 e tendo o azar extremo de comprar no pico máximo, o dinheiro foi recuperado. Não é uma situação ideal mas demostra que o periodo de investimento deve ser mais longo quanto mais agressiva a carteira.
plt.figure(figsize=(7, 6), dpi=80)
ax = sns.boxplot(data=five_years_rolling, orient="v", linewidth=1, width=0.25, fliersize=3, palette=my_pal, whis=1.5)
ax.set_title("Boxplot dos retornos rolantes a 5 anos (com outliers)")
ax.set_xlabel('')
ax.set_ylabel('Percentagem da variação dos retornos a 5 anos')
plt.ylim(-1, 24)
plt.show()
round(ten_years_rolling, 2).iplot(kind='scatter', title='Retornos rolantes a 10 anos', yTitle='Percentagem', colors=['royalblue', 'orange', 'dimgray'])
A visão dos boxplot dos retornos a 10 anos já tem uma perspectiva um pouco diferente das anteriores. As amplites de retornos a 10 anos são muito menores do que em periodos curtos e por isso já vemos uma maior concentração nos retornos esperados. No caso da carteira 4Fundos o melhor retorno a 10 anos da carteira defensiva é inferior ao pior retorno da carteira agressiva nesse mesmo período.
Como temos visto até aqui esta situação não é linear, este maior retorno da carteira agressiva é fruto de uma significativamente maior volatilidade de curto prazo, o que cria mais stress e desconforto. Para quem experiência esse desconforto o mercado recompensa com uma maior rentabilidade.
plt.figure(figsize=(7, 6), dpi=80)
ax = sns.boxplot(data=ten_years_rolling, orient="v", linewidth=1, width=0.25, fliersize=3, palette=my_pal, whis=1.5)
ax.set_title("Boxplot dos retornos rolantes a 10 anos (com outliers)")
ax.set_xlabel('')
ax.set_ylabel('Percentagem da variação dos retornos a 10 anos')
plt.show()
Se nos gráficos acima podemos ver os retornos rolantes por período e ver a maior volatilidade de retornos quanto mais arriscadas são as carteiras, nestes temos a perspectiva por carteira e podemos ver o quanto os retornos são mais estáveis quanto maior o tempo de investimento, independentemente do nível de risco da carteira.
O Bloxplot é especialmente bom para ver essa diminuição da amplitude de retornos se o tempo de investimento for maior.
Esta é uma das razões pelo qual o investimento deve ser de médio/longo prazo. A 1 ano até a defensiva teve já uma performance bastante negativa em alguns períodos, enquanto a 3 anos apenas a agressiva chegou a ter performance negativa (para quem comprou no pico da crise de 2008). Contudo, a 5 anos nem a agressiva teve performance negativa, mesmo para quem comprou no pico da crise de 2008.
# Importing .CSV of rolling returns per year
def_rolling = merge_time_series(rr_def_1, rr_def_3, how='inner')
def_rolling = merge_time_series(def_rolling, rr_def_5, how='inner')
def_rolling = merge_time_series(def_rolling, rr_def_10, how='inner') * 100
def_rolling.columns = ['1 Ano', '3 Anos', '5 Anos', '10 Anos']
ew_rolling = merge_time_series(rr_ew_1, rr_ew_3, how='inner')
ew_rolling = merge_time_series(ew_rolling, rr_ew_5, how='inner')
ew_rolling = merge_time_series(ew_rolling, rr_ew_10, how='inner') * 100
ew_rolling.columns = ['1 Ano', '3 Anos', '5 Anos', '10 Anos']
agr_rolling = merge_time_series(rr_agr_1, rr_agr_3, how='inner')
agr_rolling = merge_time_series(agr_rolling, rr_agr_5, how='inner')
agr_rolling = merge_time_series(agr_rolling, rr_agr_10, how='inner') * 100
agr_rolling.columns = ['1 Ano', '3 Anos', '5 Anos', '10 Anos']
round(def_rolling, 2).iplot(kind='scatter', title='Retornos rolantes da 4Fundos defensiva', yTitle='Percentagem', colors=['royalblue', 'orange', 'dimgray', 'indigo'])
my_pal = ["royalblue", "orange", "dimgrey", 'indigo']
plt.figure(figsize=(7, 6), dpi=80)
ax = sns.boxplot(data=def_rolling, orient="v", linewidth=1, width=0.25, fliersize=3, palette=my_pal, whis=1.5)
ax.set_title("Boxplot dos retornos rolantes da carteira defensiva (com outliers)")
ax.set_xlabel('')
ax.set_ylabel('Percentagem da variação')
plt.show()
round(ew_rolling, 2).iplot(kind='scatter', title='Retornos rolantes da 4Fundos Equal Weight', yTitle='Percentagem', colors=['royalblue', 'orange', 'dimgray', 'indigo'])
plt.figure(figsize=(7, 6), dpi=80)
ax = sns.boxplot(data=ew_rolling, orient="v", linewidth=1, width=0.25, fliersize=3, palette=my_pal, whis=1.5)
ax.set_title("Boxplot dos retornos rolantes da carteira EW (com outliers)")
ax.set_xlabel('')
ax.set_ylabel('Percentagem da variação')
plt.show()
round(agr_rolling, 2).iplot(kind='scatter', title='Retornos rolantes da 4Fundos agressiva', yTitle='Percentagem', colors=['royalblue', 'orange', 'dimgray', 'indigo'])
plt.figure(figsize=(7, 6), dpi=80)
ax = sns.boxplot(data=agr_rolling, orient="v", linewidth=1, width=0.25, fliersize=3, palette=my_pal, whis=1.5)
ax.set_title("Boxplot dos retornos rolantes da carteira agressiva (com outliers)")
ax.set_xlabel('')
ax.set_ylabel('Percentagem da variação')
plt.show()
Deixo só, por último, um gráfico da carteira 4Fundos EW (moderada) que mostra diferentes pontos de preocupação ao longo dos anos. Notícias que fizeram os investidores ficarem preocupados na altura, mostrando o que aconteceu com a carteira nos momentos e anos seguintes.
Alguns momentos criaram, de facto, quedas significativas (Falência da Lehman) que demoraram algum tempo a serem recuperadas (1 ano). Outras situações, como o episódio que ficou conhecido como Taper tantrum criaram quedas repentinas de um mês (e depois mais 3 na recuperação) que embora com pouco impacto na rentabilidade criaram situação de algum stress.
Outras, como o Brexit ou a eleição de Donald Trump parece que passaram completamente ao lado dos mercados, criando ambas apenas uma queda de um dia, sendo que posteriormente a carteira continuou a criar novos máximos.
annotations={'2008-09-15':'Falência da Lehman' ,
'2011-08-03':'S&P Downgrades U.S. Debt',
'2013-05-22':'Taper tantrum',
'2010-04-27':'S&P cuts Greek debt to junk',
'2015-06-30':'Grécia Falha' \
'<BR> pagamento ao FMI',
'2016-06-24':'Brexit',
'2016-11-08': 'Trump',
'2020-01-30': 'Coronavirus'}
round(fundos_norm['4Fundos EW'], 2).iplot(kind='scatter', title='Performance do portfolio 4Fundos EW', yTitle='Valor por cada 100€ investidos',
color='royalblue', bestfit=True)
Investir acaba por ser simples. Sabemos de antemão a forte probabilidade de uma carteira agressiva ter uma boa rentabilidade a longo pazo, até significativamente superior a uma carteira defensiva. Adicionando a isso a o crescimento exponencial dos juros compostos a muito longo prazo é uma situação significativamente mais vantajosa.
Simples não quer dizer fácil. O stress causado pela muito maior volatilidade da carteira agressiva, a possibilidade de perda de cerca de 50% do capital (mesmo que momentaneamente) numa carteira mais agressiva joga contra as rentabilidades maiores destas mesmas carteiras.
Mesmo a carteira defensiva e a curto prazo, alguns momentos como os que fiz alusão no gráfico anterior são situações que nos criam stress e são desagradáveis. Uma visão de médio/longo prazo do investimento é algo necessário nesses momentos de desconforto.
A carteira a seleccionar por cada um de nós é esse tradeoff entre maior rentabilidade de médio/longo prazo e o maior desconforto a curto prazo.
Notes: Portfolios with yearly rebalancing
annotations={'2008-09-15':'Falência da Lehman' ,
'2011-08-03':'S&P Downgrades U.S. Debt',
'2013-05-22':'Taper tantrum',
'2010-04-27':'S&P cuts Greek debt to junk',
'2015-06-30':'Grécia Falha' \
'<BR> pagamento ao FMI',
'2016-06-24':'Brexit',
'2016-11-08': 'Trump',
'2020-01-30': 'Coronavirus'}
round(, 2).iplot(kind='scatter', title='Performance de um portfolio 50% accionista e 50% obrigacionista', yTitle='Valor por cada 100€ investidos',
color='royalblue', bestfit=True)
fundo = fundos_norm[['4Fundos EW']].copy()
fundo.reset_index(inplace=True)
fundo.columns=['Date', 'Portfolio']
fundo.set_index('Date', inplace=True)
fundo_no_index = fundo.reset_index()
fundo.to_csv('WallOfWorry2.csv')
fundo_no_index
Date | Portfolio | |
---|---|---|
0 | 2007-04-20 | 100.00 |
1 | 2007-04-21 | 100.00 |
2 | 2007-04-22 | 100.00 |
3 | 2007-04-23 | 100.22 |
4 | 2007-04-24 | 99.90 |
... | ... | ... |
5109 | 2021-04-15 | 260.13 |
5110 | 2021-04-16 | 260.61 |
5111 | 2021-04-17 | 260.61 |
5112 | 2021-04-18 | 260.61 |
5113 | 2021-04-19 | 260.61 |
5114 rows × 2 columns
from sklearn import linear_model
reg = linear_model.LinearRegression()
reg.fit(fundo_no_index[['Portfolio']], fundo_no_index['Date'])
# reg.coef_
LinearRegression()
spector_data
<class 'statsmodels.datasets.utils.Dataset'>
print('Predicted values: ', res.predict())
Predicted values: [-0.05426921 0.07340692 0.27529932 -0.01762875 0.57778716 0.00701576 -0.03936941 0.05363477 0.16983152 0.62464001 -0.06818476 0.28335827 0.39932119 0.27651741 0.41225249 -0.0276562 0.03995305 0.01409045 0.56914272 0.60868703 0.06696482 0.85354417 0.36800073 0.78153024 0.77445555 0.47660622 0.63141194 0.37090458 0.79399386 0.9773322 0.53887544 0.1885505 ]
# imports
import pandas as pd
import statsmodels.api as sm
import numpy as np
# data
np.random.seed(123)
# assign dependent and independent / explanatory variables
variables = list(fundo_no_index.columns)
y = 'Date'
x = [var for var in variables if var not in y ]
# Ordinary least squares regression
model_Simple = sm.OLS(fundo_no_index[y], fundo_no_index[x]).fit()
# Add a constant term like so:
model = sm.OLS(fundo_no_index[y], sm.add_constant(fundo_no_index[x])).fit()
model.params
const 1,007,854,171,425,378,816.00 Portfolio 2,484,214,206,361,961.50 dtype: object
fundo_no_index.columns
Index(['Date', 'Portfolio'], dtype='object')
from sklearn import linear_model
reg = linear_model.LinearRegression()
reg.fit(fundo_no_index[['Portfolio']], fundo_no_index['Date'])
LinearRegression()
reg.coef_
array([2.48421421e+15])
print(reg.intercept_)
1.0078541714253793e+18