From aab448f187a1c80f68d99f4832d51e310d8b90f2 Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Mon, 12 Oct 2015 16:05:12 -0700 Subject: [PATCH 01/11] ENH add ability to plot multiple stdev regions for the OOS cone in plotting.plot_rolling_returns. Update calling functions to use default of plotting these multiple regions: 1.0, 1.5, 2.0 stdevs --- pyfolio/plotting.py | 36 +++++++++++++++++++++++++++++++----- pyfolio/tears.py | 4 ++-- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index ac61e7e6..9adbc0dc 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -611,10 +611,18 @@ 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) + # check to see if we're just rendering a single cone, or multiple + # cones at various stddevs, defined as elements of cone_std + if type(cone_std) is list: + cone_df = timeseries.cone_rolling( + returns, + num_stdev=cone_std[0], + cone_fit_end_date=live_start_date) + else: + 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] @@ -641,7 +649,25 @@ def plot_rolling_returns( ax.fill_between(cone_df_live.index, cone_df_live.sd_down, cone_df_live.sd_up, - color='red', alpha=0.30) + color='steelblue', alpha=0.25) + + if type(cone_std) is list: + for cone_i in range(1, len(cone_std)): + cone_df = timeseries.cone_rolling( + returns, + num_stdev=cone_std[cone_i], + 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]] + + ax.fill_between(cone_df_live.index, + cone_df_live.sd_down, + cone_df_live.sd_up, + color='steelblue', alpha=0.25) ax.axhline(1.0, linestyle='--', color='black', lw=2) ax.set_ylabel('Cumulative returns') diff --git a/pyfolio/tears.py b/pyfolio/tears.py index 327ce08e..d8c70021 100644 --- a/pyfolio/tears.py +++ b/pyfolio/tears.py @@ -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. @@ -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): """ From b81e207a15108c644d64b85f4d34874fe00cdc18 Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Mon, 12 Oct 2015 16:20:47 -0700 Subject: [PATCH 02/11] STY PEP8 --- pyfolio/plotting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index 9adbc0dc..9f1049cf 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -661,8 +661,8 @@ def plot_rolling_returns( 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]] + temp_keep_days = cone_df_live.index < returns.index[-1] + cone_df_live = cone_df_live[temp_keep_days] ax.fill_between(cone_df_live.index, cone_df_live.sd_down, From 3cffef4f408b28b1bae1d959f1227fcee127c080 Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Mon, 12 Oct 2015 17:09:57 -0700 Subject: [PATCH 03/11] STY Python3 compat --- pyfolio/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index 9f1049cf..cce2473b 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -652,7 +652,7 @@ def plot_rolling_returns( color='steelblue', alpha=0.25) if type(cone_std) is list: - for cone_i in range(1, len(cone_std)): + for cone_i in list(range(1, len(cone_std))): cone_df = timeseries.cone_rolling( returns, num_stdev=cone_std[cone_i], From efcf5ae92fcc109d0f0e41f34f76b28ff11cf641 Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Tue, 13 Oct 2015 18:06:21 -0700 Subject: [PATCH 04/11] ENH in plotting.plot_rolling_returns, factor out cone drawing into local function. Turn off cones completely if plotting volatility match --- pyfolio/plotting.py | 68 +++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index cce2473b..688cb8d5 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -573,6 +573,24 @@ def plot_rolling_returns( The axes that were plotted on. """ + def draw_cone(returns, num_stdev, live_start_date, ax): + cone_df = timeseries.cone_rolling( + returns, + num_stdev=num_stdev, + 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] + temp_keep_days = cone_df_live.index < returns.index[-1] + cone_df_live = cone_df_live[temp_keep_days] + + ax.fill_between(cone_df_live.index, + cone_df_live.sd_down, + cone_df_live.sd_up, + color='steelblue', alpha=0.25) + + return cone_df_fit, cone_df_live if ax is None: ax = plt.gca() @@ -610,24 +628,19 @@ def plot_rolling_returns( lw=4, color='red', alpha=0.6, label='Live', ax=ax, **kwargs) - if cone_std is not None: - # check to see if we're just rendering a single cone, or multiple - # cones at various stddevs, defined as elements of cone_std - if type(cone_std) is list: - cone_df = timeseries.cone_rolling( - returns, - num_stdev=cone_std[0], - cone_fit_end_date=live_start_date) + if cone_std is not None and not volatility_match: + # 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_list = [cone_std] else: - 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_std_list = cone_std - cone_df_live = cone_df[cone_df.index > live_start_date] - cone_df_live = cone_df_live[cone_df_live.index < returns.index[-1]] + for cone_i in cone_std_list: + cone_df_fit, cone_df_live = draw_cone(returns, + cone_i, + live_start_date, + ax) cone_df_fit['line'].plot( ax=ax, @@ -646,29 +659,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='steelblue', alpha=0.25) - - if type(cone_std) is list: - for cone_i in list(range(1, len(cone_std))): - cone_df = timeseries.cone_rolling( - returns, - num_stdev=cone_std[cone_i], - 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] - temp_keep_days = cone_df_live.index < returns.index[-1] - cone_df_live = cone_df_live[temp_keep_days] - - ax.fill_between(cone_df_live.index, - cone_df_live.sd_down, - cone_df_live.sd_up, - color='steelblue', alpha=0.25) - ax.axhline(1.0, linestyle='--', color='black', lw=2) ax.set_ylabel('Cumulative returns') ax.set_title('Cumulative Returns') From 8da21929e8a1f056ef6080346bd9365910cbca06 Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Tue, 13 Oct 2015 18:09:38 -0700 Subject: [PATCH 05/11] DOC update docstring --- pyfolio/plotting.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index 688cb8d5..cf7a5bb4 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -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 From 8300be51608a6968d62d13bff2f8800c32fb036d Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Wed, 14 Oct 2015 10:34:14 -0700 Subject: [PATCH 06/11] STY PEP8 and code cleanup from CR recommendations --- pyfolio/plotting.py | 46 +++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index cf7a5bb4..89a5c93e 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -580,18 +580,17 @@ def draw_cone(returns, num_stdev, live_start_date, ax): num_stdev=num_stdev, 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] - temp_keep_days = cone_df_live.index < returns.index[-1] - cone_df_live = cone_df_live[temp_keep_days] - - ax.fill_between(cone_df_live.index, - cone_df_live.sd_down, - cone_df_live.sd_up, + 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_df_fit, cone_df_live + return cone_in_sample, cone_out_of_sample if ax is None: ax = plt.gca() @@ -633,17 +632,20 @@ def draw_cone(returns, num_stdev, live_start_date, ax): # 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_list = [cone_std] - else: - cone_std_list = cone_std - - for cone_i in cone_std_list: - cone_df_fit, cone_df_live = draw_cone(returns, - cone_i, - live_start_date, - ax) - - cone_df_fit['line'].plot( + #cone_std_list = [cone_std] + cone_std = [cone_std] + #else: + # cone_std_list = cone_std + + #for cone_i in cone_std_list: + 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', @@ -651,7 +653,7 @@ def draw_cone(returns, num_stdev, live_start_date, ax): color='forestgreen', alpha=0.7, **kwargs) - cone_df_live['line'].plot( + cone_out_of_sample['line'].plot( ax=ax, ls='--', label='Predicted trend', From b3c221549a3c20f51b150921b980e6567cc5a99d Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Wed, 14 Oct 2015 10:44:53 -0700 Subject: [PATCH 07/11] FIX reverse hardcoding of turning off cone in volatility matched plot in ploting.plot_rolling_retuns(), and specify it turned off in the calling functions tears.create_returns_tear_sheet() --- pyfolio/plotting.py | 2 +- pyfolio/tears.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index 89a5c93e..92e40a0c 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -628,7 +628,7 @@ def draw_cone(returns, num_stdev, live_start_date, ax): lw=4, color='red', alpha=0.6, label='Live', ax=ax, **kwargs) - if cone_std is not None and not volatility_match: + if cone_std is not None: # 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): diff --git a/pyfolio/tears.py b/pyfolio/tears.py index d8c70021..c5ba90c0 100644 --- a/pyfolio/tears.py +++ b/pyfolio/tears.py @@ -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( From 2a0c49c4c77e70bc504e6f6368b8423ee05391a9 Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Wed, 14 Oct 2015 10:50:43 -0700 Subject: [PATCH 08/11] Added TO-DO comment/question in plotting.plot_rolling_returns() for how to properly address Issue #152 --- pyfolio/plotting.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index 92e40a0c..ef21a3bc 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -600,6 +600,11 @@ def draw_cone(returns, num_stdev, live_start_date, ax): '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: + # (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( (returns / returns.std()) * bmark_vol, 1.0 From cf83da207f43e1014bf9034d20bc2544afa4eee6 Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Wed, 14 Oct 2015 10:59:40 -0700 Subject: [PATCH 09/11] STY delete stale comments leftover from debugging --- pyfolio/plotting.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index ef21a3bc..d991b174 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -637,12 +637,8 @@ def draw_cone(returns, num_stdev, live_start_date, ax): # 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_list = [cone_std] cone_std = [cone_std] - #else: - # cone_std_list = cone_std - #for cone_i in cone_std_list: for cone_i in cone_std: cone_in_sample, cone_out_of_sample = draw_cone( returns, From 50540ff55c21eb779f5806ec9873ab0d5eae2f35 Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Fri, 16 Oct 2015 08:07:08 -0700 Subject: [PATCH 10/11] STY change input cone_std in plotting.plot_rolling_returns to be tuple --- pyfolio/plotting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index d991b174..5c605387 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -552,9 +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, or list, optional + cone_std : float, or tuple, 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 + 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 From 62958f576b8b0e3b883d3d5ad4405e9381bba107 Mon Sep 17 00:00:00 2001 From: Justin Lent Date: Fri, 16 Oct 2015 08:40:21 -0700 Subject: [PATCH 11/11] FIX update all callers of plotting.plot_rolling_returns to pass tuples. Fix cone rendering in volatility weighted case --- pyfolio/plotting.py | 14 +++----------- pyfolio/tears.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/pyfolio/plotting.py b/pyfolio/plotting.py index 5c605387..f779b476 100644 --- a/pyfolio/plotting.py +++ b/pyfolio/plotting.py @@ -600,17 +600,9 @@ def draw_cone(returns, num_stdev, live_start_date, ax): '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: - # (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( - (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)) diff --git a/pyfolio/tears.py b/pyfolio/tears.py index c5ba90c0..58cce5f2 100644 --- a/pyfolio/tears.py +++ b/pyfolio/tears.py @@ -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, 1.5, 2.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. @@ -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 @@ -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, 1.5, 2.0], + cone_std=(1.0, 1.5, 2.0), benchmark_rets=None, return_fig=False): """ @@ -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