-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Multistrike cones #310
Multistrike cones #310
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,8 @@ | |
import matplotlib.pyplot as plt | ||
from matplotlib.ticker import FuncFormatter | ||
import matplotlib.lines as mlines | ||
from matplotlib import figure | ||
from matplotlib.backends.backend_agg import FigureCanvasAgg | ||
|
||
from sklearn import preprocessing | ||
|
||
|
@@ -1660,3 +1662,102 @@ def plot_prob_profit_trade(round_trips, ax=None): | |
xlim=(lower_plot, upper_plot), ylim=(0, y.max() + 1.)) | ||
|
||
return ax | ||
|
||
|
||
def plot_multistrike_cones(is_returns, oos_returns, num_samples=1000, | ||
name=None, ax=None, cone_std=(1., 1.5, 2.), | ||
random_seed=None, num_strikes=0): | ||
""" | ||
Plots the upper and lower bounds of an n standard deviation | ||
cone of forecasted cumulative returns. This cone is non-parametric, | ||
meaning it does not assume that returns are normally distributed. Redraws | ||
a new cone when returns fall outside of last cone drawn. | ||
|
||
Parameters | ||
---------- | ||
is_returns : pandas.core.frame.DataFrame | ||
Non-cumulative in-sample returns. | ||
oos_returns : pandas.core.frame.DataFrame | ||
Non-cumulative out-of-sample returns. | ||
num_samples : int | ||
Number of samples to draw from the in-sample daily returns. | ||
Each sample will be an array with length num_days. | ||
A higher number of samples will generate a more accurate | ||
bootstrap cone. | ||
name : str, optional | ||
Plot title | ||
ax : matplotlib.Axes, optional | ||
Axes upon which to plot. | ||
cone_std : list of int/float | ||
Number of standard devations to use in the boundaries of | ||
the cone. If multiple values are passed, cone bounds will | ||
be generated for each value. | ||
random_seed : int | ||
Seed for the pseudorandom number generator used by the pandas | ||
sample method. | ||
num_strikes : int | ||
Upper limit for number of cones drawn. Can be anything from 0 to 3. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor nit on the docstring here - if num_strikes is 0, then we'll still have one cone drawn(even though we say in the docstring that the upper limit is 0). What do you think of rephrasing to something like:
|
||
|
||
|
||
Returns | ||
------- | ||
Returns are either an ax or fig option, but not both. If a | ||
matplotlib.Axes instance is passed in as ax, then it will be modified | ||
and returned. This allows for users to plot interactively in jupyter | ||
notebook. When no ax object is passed in, a matplotlib.figure instance | ||
is generated and returned. This figure can then be used to save | ||
the plot as an image without viewing it. | ||
|
||
ax : matplotlib.Axes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This confused me a bit, because it feels like we're returning both |
||
The axes that were plotted on. | ||
fig : matplotlib.figure | ||
The figure instance which contains all the plot elements. | ||
""" | ||
if ax is None: | ||
fig = figure.Figure(figsize=(10, 8)) | ||
FigureCanvasAgg(fig) | ||
axes = fig.add_subplot(111) | ||
else: | ||
axes = ax | ||
|
||
returns = timeseries.cum_returns(oos_returns, starting_value=1.) | ||
bounds = timeseries.forecast_cone_bootstrap(is_returns, | ||
len(oos_returns), | ||
cone_std=cone_std, | ||
num_samples=num_samples, | ||
random_seed=random_seed) | ||
bounds.index = oos_returns.index | ||
bounds_tmp = bounds.copy() | ||
returns_tmp = returns.copy() | ||
cone_start = returns.index[0] | ||
colors = ["green", "orange", "orangered", "darkred"] | ||
|
||
for c in range(num_strikes+1): | ||
if c > 0: | ||
tmp = returns.loc[cone_start:] | ||
crossing = (tmp < bounds_tmp[float(-2.)].iloc[:len(tmp)]) | ||
if crossing.sum() <= 0: | ||
break | ||
cone_start = crossing.loc[crossing].index[0] | ||
returns_tmp = oos_returns.loc[cone_start:] | ||
bounds_tmp = (bounds - (1 - returns.loc[cone_start])) | ||
for std in cone_std: | ||
x = returns_tmp.index | ||
y1 = bounds_tmp[float(std)].iloc[:len(returns_tmp)] | ||
y2 = bounds_tmp[float(-std)].iloc[:len(returns_tmp)] | ||
ax.fill_between(x, y1, y2, color=colors[c], alpha=0.5) | ||
# Plot returns line graph | ||
returns.plot(ax=axes, | ||
lw=3., | ||
color='black', | ||
label='Cumulative returns = {:.2f}%'.format( | ||
(returns.iloc[-1] - 1) * 100)) | ||
if name is not None: | ||
axes.set_title(name) | ||
axes.axhline(1, color='black', alpha=0.2) | ||
axes.legend() | ||
|
||
if ax is None: | ||
return fig | ||
else: | ||
return axes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it currently stands, this function can plot up to 3 strikes, depending on how the out-of-sample returns behave - we should either update the function name and/or docstring to make it clear that it's a fixed number of strikes, or allow the user to pass in a parameter to determine how many strikes they care about. OK with either, maybe the former is better in terms of limiting scope?