Skip to content
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

ENH Ability to make cones with multiple shades stdev regions #168

Closed
wants to merge 11 commits into from
58 changes: 39 additions & 19 deletions pyfolio/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,9 @@ def plot_rolling_returns(
live_start_date : datetime, optional
The point in time when the strategy began live trading, after
its backtest period.
cone_std : float, optional
The standard deviation to use for the cone plots.
cone_std : float, or list, optional
If float, The standard deviation to use for the cone plots.
If list, A list of standard deviation values to use for the cone plots
- The cone is a normal distribution with this standard deviation
centered around a linear regression.
legend_loc : matplotlib.loc, optional
Expand All @@ -573,6 +574,23 @@ def plot_rolling_returns(
The axes that were plotted on.

"""
def draw_cone(returns, num_stdev, live_start_date, ax):
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason to prepend the variable with num_ here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because I wanted to be consistent with the parameter name in timeseries.cone_rolling() which is the only place it gets passed. Is it worth changing for simplicity, or worth keeping because better to be consistent with where it is used downstream? thoughts?

cone_df = timeseries.cone_rolling(
returns,
num_stdev=num_stdev,
cone_fit_end_date=live_start_date)

cone_in_sample = cone_df[cone_df.index < live_start_date]
cone_out_of_sample = cone_df[cone_df.index > live_start_date]
cone_out_of_sample = cone_out_of_sample[
cone_out_of_sample.index < returns.index[-1]]

ax.fill_between(cone_out_of_sample.index,
cone_out_of_sample.sd_down,
cone_out_of_sample.sd_up,
color='steelblue', alpha=0.25)

return cone_in_sample, cone_out_of_sample

if ax is None:
ax = plt.gca()
Expand All @@ -582,6 +600,11 @@ def plot_rolling_returns(
'factor_returns.')
elif volatility_match and factor_returns is not None:
bmark_vol = factor_returns.loc[returns.index].std()
# TO-DO: @tweicki 'returns' probably needs to get updated to:
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, if volatility_match is True, returns need to be changed/re-assigned as you proposed. That will fix the vol match plot.

# (returns / returns.std()) * bmark_vol if we want to plot
# the cone on this later on.
# Will we need a temp variable to do this? Or can we just re-assign as
# returns = (returns / returns.std()) * bmark_vol
df_cum_rets = timeseries.cum_returns(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can then get rid of this line.

(returns / returns.std()) * bmark_vol,
1.0
Expand Down Expand Up @@ -611,25 +634,27 @@ def plot_rolling_returns(
label='Live', ax=ax, **kwargs)

if cone_std is not None:
cone_df = timeseries.cone_rolling(
returns,
num_stdev=cone_std,
cone_fit_end_date=live_start_date)

cone_df_fit = cone_df[cone_df.index < live_start_date]

cone_df_live = cone_df[cone_df.index > live_start_date]
cone_df_live = cone_df_live[cone_df_live.index < returns.index[-1]]

cone_df_fit['line'].plot(
# check to see if cone_std was passed as a single value and,
# if so, just convert to list automatically
if isinstance(cone_std, float):
cone_std = [cone_std]

for cone_i in cone_std:
cone_in_sample, cone_out_of_sample = draw_cone(
returns,
cone_i,
live_start_date,
ax)

cone_in_sample['line'].plot(
ax=ax,
ls='--',
label='Backtest trend',
lw=2,
color='forestgreen',
alpha=0.7,
**kwargs)
cone_df_live['line'].plot(
cone_out_of_sample['line'].plot(
ax=ax,
ls='--',
label='Predicted trend',
Expand All @@ -638,11 +663,6 @@ def plot_rolling_returns(
alpha=0.7,
**kwargs)

ax.fill_between(cone_df_live.index,
cone_df_live.sd_down,
cone_df_live.sd_up,
color='red', alpha=0.30)

ax.axhline(1.0, linestyle='--', color='black', lw=2)
ax.set_ylabel('Cumulative returns')
ax.set_title('Cumulative Returns')
Expand Down
6 changes: 3 additions & 3 deletions pyfolio/tears.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def create_full_tear_sheet(returns, positions=None, transactions=None,
benchmark_rets=None,
gross_lev=None,
live_start_date=None, bayesian=False,
cone_std=1.0, set_context=True):
cone_std=[1.0, 1.5, 2.0], set_context=True):
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor stylistic but I'd change these to be tuples.

"""
Generate a number of tear sheets that are useful
for analyzing a strategy's performance.
Expand Down Expand Up @@ -140,7 +140,7 @@ def create_full_tear_sheet(returns, positions=None, transactions=None,

@plotting_context
def create_returns_tear_sheet(returns, live_start_date=None,
cone_std=1.0,
cone_std=[1.0, 1.5, 2.0],
benchmark_rets=None,
return_fig=False):
"""
Expand Down Expand Up @@ -230,7 +230,7 @@ def create_returns_tear_sheet(returns, live_start_date=None,
returns,
factor_returns=benchmark_rets,
live_start_date=live_start_date,
cone_std=cone_std,
cone_std=None,
volatility_match=True,
ax=ax_rolling_returns_vol_match)
ax_rolling_returns_vol_match.set_title(
Expand Down