Skip to content

Perf attrib tearsheets #441

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 28 additions & 35 deletions pyfolio/perf_attrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import matplotlib.pyplot as plt
from pyfolio.pos import get_percent_alloc
from pyfolio.utils import print_table, set_legend_location, COLORS
from pyfolio.utils import print_table, set_legend_location


def perf_attrib(returns, positions, factor_returns, factor_loadings,
Expand Down Expand Up @@ -151,35 +151,38 @@ def create_perf_attrib_stats(perf_attrib):


def show_perf_attrib_stats(returns, positions, factor_returns,
factor_loadings):
factor_loadings, pos_in_dollars=True):
"""
Calls `perf_attrib` using inputs, and displays outputs using
`utils.print_table`.
"""
risk_exposures, perf_attrib_data = perf_attrib(returns,
positions,
factor_returns,
factor_loadings)
risk_exposures, perf_attrib_data = perf_attrib(
returns,
positions,
factor_returns,
factor_loadings,
pos_in_dollars=pos_in_dollars,
)

perf_attrib_stats = create_perf_attrib_stats(perf_attrib_data)
print_table(perf_attrib_stats)
print_table(risk_exposures)


def plot_returns(returns, specific_returns, common_returns, ax=None):
def plot_returns(perf_attrib_data, ax=None):
"""
Plot total, specific, and common returns.

Parameters
----------
returns : pd.Series
total returns, indexed by datetime

specific_returns : pd.Series
specific returns, indexed by datetime

commons_returns : pd.Series
common returns, indexed by datetime
perf_attrib_data : pd.DataFrame
df with factors, common returns, and specific returns as columns,
and datetimes as index
- Example:
momentum reversal common_returns specific_returns
dt
2017-01-01 0.249087 0.935925 1.185012 1.185012
2017-01-02 -0.003194 -0.400786 -0.403980 -0.403980

ax : matplotlib.axes.Axes
axes on which plots are made. if None, current axes will be used
Expand All @@ -191,13 +194,17 @@ def plot_returns(returns, specific_returns, common_returns, ax=None):
if ax is None:
ax = plt.gca()

returns = perf_attrib_data['total_returns']
specific_returns = perf_attrib_data['specific_returns']
common_returns = perf_attrib_data['common_returns']

ax.plot(ep.cum_returns(returns), color='g', label='Total returns')
ax.plot(ep.cum_returns(specific_returns), color='b',
label='Cumulative specific returns')
ax.plot(ep.cum_returns(common_returns), color='r',
label='Cumulative common returns')

ax.set_title('Time Series of cumulative returns')
ax.set_title('Time series of cumulative returns')
ax.set_ylabel('Returns')

set_legend_location(ax)
Expand Down Expand Up @@ -235,20 +242,12 @@ def plot_alpha_returns(alpha_returns, ax=None):
return ax


def plot_factor_contribution_to_perf(exposures, perf_attrib_data, ax=None):
def plot_factor_contribution_to_perf(perf_attrib_data, ax=None):
"""
Plot each factor's contribution to performance.

Parameters
----------
exposures : pd.DataFrame
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this was just unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup

df indexed by datetime, with factors as columns
- Example:
momentum reversal
dt
2017-01-01 -0.238655 0.077123
2017-01-02 0.821872 1.520515

perf_attrib_data : pd.DataFrame
df with factors, common returns, and specific returns as columns,
and datetimes as index
Expand All @@ -271,12 +270,8 @@ def plot_factor_contribution_to_perf(exposures, perf_attrib_data, ax=None):
factors_and_specific = perf_attrib_data.drop(
['total_returns', 'common_returns'], axis='columns')

ax.stackplot(
factors_and_specific.index,
[factors_and_specific[s] for s in factors_and_specific],
labels=factors_and_specific.columns,
colors=COLORS
)
for col in factors_and_specific:
ax.plot(factors_and_specific[col])

ax.axhline(0, color='k')
set_legend_location(ax)
Expand Down Expand Up @@ -309,10 +304,8 @@ def plot_risk_exposures(exposures, ax=None):
if ax is None:
ax = plt.gca()

ax.stackplot(exposures.index,
[exposures[s] for s in exposures],
labels=exposures.columns,
colors=COLORS)
for col in exposures:
ax.plot(exposures[col])

set_legend_location(ax)
ax.set_ylabel('Factor exposures')
Expand Down
81 changes: 80 additions & 1 deletion pyfolio/tears.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from . import _seaborn as sns
from . import capacity
from . import perf_attrib
from . import plotting
from . import pos
from . import risk
Expand Down Expand Up @@ -76,7 +77,11 @@ def create_full_tear_sheet(returns,
volumes=None,
percentile=None,
turnover_denom='AGB',
set_context=True):
set_context=True,
factor_returns=None,
factor_loadings=None,
pos_in_dollars=True,
return_fig=False):
"""
Generate a number of tear sheets that are useful
for analyzing a strategy's performance.
Expand Down Expand Up @@ -159,6 +164,13 @@ def create_full_tear_sheet(returns,
set_context : boolean, optional
If True, set default plotting style context.
- See plotting.context().
factor_returns : pd.Dataframe, optional
Returns by factor, with date as index and factors as columns
factor_loadings : pd.Dataframe, optional
Factor loadings for all days in the date range, with date and
ticker as index, and factors as columns.
pos_in_dollars : boolean, optional
indicates whether positions is in dollars
"""

if benchmark_rets is None:
Expand Down Expand Up @@ -218,6 +230,11 @@ def create_full_tear_sheet(returns,
create_risk_tear_sheet(positions, style_factor_panel, sectors,
caps, shares_held, volumes, percentile)

if factor_returns is not None and factor_loadings is not None:
create_perf_attrib_tearsheet(returns, positions, factor_returns,
factor_loadings,
pos_in_dollars=pos_in_dollars)

if bayesian:
create_bayesian_tear_sheet(returns,
live_start_date=live_start_date,
Expand Down Expand Up @@ -1432,3 +1449,65 @@ def create_risk_tear_sheet(positions,
plt.show()
if return_fig:
return fig


@plotting.customize
def create_perf_attrib_tearsheet(returns,
positions,
factor_returns,
factor_loadings,
pos_in_dollars=True,
return_fig=False):
"""
Generate plots and tables for analyzing a strategy's performance.

Parameters
----------
returns : pd.Series
Returns for each day in the date range.

positions: pd.DataFrame
Daily holdings (in dollars or percentages), indexed by date.
Will be converted to percentages if positions are in dollars.
Short positions show up as cash in the 'cash' column.

factor_returns : pd.DataFrame
Returns by factor, with date as index and factors as columns

factor_loadings : pd.DataFrame
Factor loadings for all days in the date range, with date
and ticker as index, and factors as columns.

pos_in_dollars : boolean, optional
Flag indicating whether `positions` are in dollars or percentages
If True, positions are in dollars.

return_fig : boolean, optional
If True, returns the figure that was plotted on.
"""
portfolio_exposures, perf_attrib_data = perf_attrib.perf_attrib(
returns, positions, factor_returns, factor_loadings,
pos_in_dollars=pos_in_dollars
)

# aggregate perf attrib stats and show summary table
perf_attrib.show_perf_attrib_stats(returns, positions, factor_returns,
factor_loadings)

vertical_sections = 3
fig = plt.figure(figsize=[14, vertical_sections * 6])
gs = gridspec.GridSpec(vertical_sections, 1, wspace=0.5, hspace=0.5)

perf_attrib.plot_returns(perf_attrib_data, ax=plt.subplot(gs[0]))

perf_attrib.plot_factor_contribution_to_perf(perf_attrib_data,
ax=plt.subplot(gs[1]))

perf_attrib.plot_risk_exposures(portfolio_exposures,
ax=plt.subplot(gs[2]))

gs.tight_layout(fig)

plt.show()
if return_fig:
return fig
7 changes: 6 additions & 1 deletion pyfolio/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ def get_symbol_rets(symbol, start=None, end=None):
end=end)


def set_legend_location(ax):
def set_legend_location(ax, autofmt_xdate=True):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever pass autofmt_xdate=False?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't as of now, I wanted to pass it as an option in case we want to handle formatting elsewhere in future plots

"""
Put legend in right of plot instead of overlapping with it.
"""
Expand All @@ -622,3 +622,8 @@ def set_legend_location(ax):

ax.legend(frameon=True, framealpha=0.5, loc='upper left',
bbox_to_anchor=(1.05, 1))

ax.set_prop_cycle('color', COLORS)

if autofmt_xdate:
ax.figure.autofmt_xdate()