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
62 changes: 37 additions & 25 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 tuple, optional
If float, The standard deviation to use for the cone plots.
If tuple, Tuple 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,12 +600,9 @@ def plot_rolling_returns(
'factor_returns.')
elif volatility_match and factor_returns is not None:
bmark_vol = factor_returns.loc[returns.index].std()
df_cum_rets = timeseries.cum_returns(
(returns / returns.std()) * bmark_vol,
1.0
)
else:
df_cum_rets = timeseries.cum_returns(returns, 1.0)
returns = (returns / returns.std()) * bmark_vol

df_cum_rets = timeseries.cum_returns(returns, 1.0)

y_axis_formatter = FuncFormatter(utils.one_dec_places)
ax.yaxis.set_major_formatter(FuncFormatter(y_axis_formatter))
Expand All @@ -611,25 +626,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 +655,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
16 changes: 9 additions & 7 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):
"""
Generate a number of tear sheets that are useful
for analyzing a strategy's performance.
Expand Down Expand Up @@ -94,8 +94,9 @@ def create_full_tear_sheet(returns, positions=None, transactions=None,
after its backtest period.
bayesian: boolean, optional
If True, causes the generation of a Bayesian tear sheet.
cone_std : float, optional
The standard deviation to use for the cone plots.
cone_std : float, or tuple, optional
If float, The standard deviation to use for the cone plots.
If tuple, Tuple 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.
set_context : boolean, optional
Expand Down Expand Up @@ -140,7 +141,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 All @@ -162,8 +163,9 @@ def create_returns_tear_sheet(returns, live_start_date=None,
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 tuple, optional
If float, The standard deviation to use for the cone plots.
If tuple, Tuple 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.
benchmark_rets : pd.Series, optional
Expand Down Expand Up @@ -230,7 +232,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