Skip to content

Commit

Permalink
ENH Added slippage sweep and slippage sensitivity plots. Added suppor…
Browse files Browse the repository at this point in the history
…t for slippage adjustement in all full tearsheet plots.

STY Moved turnover to txn.py. Moved transactions related tests to separate file
  • Loading branch information
a-campbell committed Oct 16, 2015
1 parent 1da0d9c commit 485e8ff
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 4 deletions.
108 changes: 108 additions & 0 deletions pyfolio/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from . import utils
from . import timeseries
from . import pos
from . import txn

from .utils import APPROX_BDAYS_PER_MONTH

Expand Down Expand Up @@ -1088,6 +1089,113 @@ def plot_turnover(returns, transactions, positions,
return ax


def plot_slippage_sweep(returns, transactions, positions,
slippage_params=None, ax=None, **kwargs):
"""Plots a equity curves at different per-dollar slippage assumptions.
Parameters
----------
returns : pd.Series
Timeseries of portfolio returns to be adjusted for various
degrees of slippage.
transactions : pd.DataFrame
Daily transaction volume and dollar ammount.
- See full explanation in tears.create_full_tear_sheet.
positions : pd.DataFrame
Daily net position values.
- See full explanation in tears.create_full_tear_sheet.
slippage_params: list
Slippage pameters to apply to the return time series (in
basis points).
ax : matplotlib.Axes, optional
Axes upon which to plot.
**kwargs, optional
Passed to seaborn plotting function.
Returns
-------
ax : matplotlib.Axes
The axes that were plotted on.
"""
if ax is None:
ax = plt.gca()

turnover = pos.get_turnover(transactions, positions, period=None)
# Multiply average long/short turnover by two to get the total
# value of buys and sells each day.
turnover *= 2

if slippage_params is None:
slippage_params = [3, 8, 10, 12, 15, 20, 50]

slippage_sweep = pd.DataFrame()
for bps in slippage_params:
adj_returns = txn.adjust_returns_for_slippage(returns, turnover, bps)
label = str(bps) + " bps"
slippage_sweep[label] = timeseries.cum_returns(adj_returns, 1)

slippage_sweep.plot(alpha=1.0, lw=0.5, ax=ax)

ax.set_title('Cumulative Returns Given Per-Dollar Slippage Assumption')
df_cum_rets = timeseries.cum_returns(returns, starting_value=1)
ax.set_xlim((df_cum_rets.index[0], df_cum_rets.index[-1]))
ax.legend(loc='center left')

return ax


def plot_slippage_sensitivity(returns, transactions, positions,
ax=None, **kwargs):
"""Plots curve relating per-dollar slippage to average annual returns.
Parameters
----------
returns : pd.Series
Timeseries of portfolio returns to be adjusted for various
degrees of slippage.
transactions : pd.DataFrame
Daily transaction volume and dollar ammount.
- See full explanation in tears.create_full_tear_sheet.
slippage_params: list
Slippage pameters to apply to the return time series (in
basis points).
ax : matplotlib.Axes, optional
Axes upon which to plot.
**kwargs, optional
Passed to seaborn plotting function.
Returns
-------
ax : matplotlib.Axes
The axes that were plotted on.
"""
if ax is None:
ax = plt.gca()

turnover = pos.get_turnover(transactions, positions, period=None)
# Multiply average long/short turnover by two to get the total
# value of buys and sells each day.
turnover *= 2
avg_returns_given_slippage = pd.Series()
for bps in range(1, 100):
adj_returns = txn.adjust_returns_for_slippage(returns, turnover, bps)
avg_returns = timeseries.sharpe_ratio(

This comment has been minimized.

Copy link
@humdings

humdings Oct 21, 2015

Contributor

@acampbell22 @twiecki I thought I had made this comment already but I guess it didn't post.

This function is plotting the sensitivity of the sharpe ratio to slippage, not the sensitivity of the annual returns.

I'm not sure if the docstring or the code should be changed, but they should agree.

adj_returns, returns_style='calendar')
avg_returns_given_slippage.loc[bps] = avg_returns

avg_returns_given_slippage.plot(alpha=1.0, lw=2, ax=ax)

ax.set_title('Average Annual Returns Given Per-Dollar Slippage Assumption')
# ax.tick_params(axis='x', which='major', labelsize=10)
ax.set_xticks(np.arange(0, 100, 10))
ax.set_ylabel('Average Annual Return')
ax.set_xlabel('Per-Dollar Slippage (bps)')

return ax


def plot_daily_turnover_hist(transactions, positions,
ax=None, **kwargs):
"""Plots a histogram of daily turnover rates.
Expand Down
43 changes: 39 additions & 4 deletions pyfolio/tears.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from . import timeseries
from . import utils
from . import pos
from . import txn
from . import plotting
from .plotting import plotting_context

Expand All @@ -41,6 +42,7 @@
def create_full_tear_sheet(returns, positions=None, transactions=None,
benchmark_rets=None,
gross_lev=None,
slippage=None,
live_start_date=None, bayesian=False,
sector_mappings=None,
cone_std=1.0, set_context=True):
Expand Down Expand Up @@ -90,6 +92,13 @@ def create_full_tear_sheet(returns, positions=None, transactions=None,
2009-12-07 0.999783
2009-12-08 0.999880
2009-12-09 1.000283
slippage : int/float, optional
Basis points of slippage to apply to returns before generating
tearsheet stats and plots.
If a value is provided, slippage parameter sweep
plots will be generated from the unadjusted returns.
Transactions and positions must also be passed.
- See txn.adjust_returns_for_slippage for more details.
live_start_date : datetime, optional
The point in time when the strategy began live trading,
after its backtest period.
Expand All @@ -111,6 +120,16 @@ def create_full_tear_sheet(returns, positions=None, transactions=None,
if returns.index[0] < benchmark_rets.index[0]:
returns = returns[returns.index > benchmark_rets.index[0]]

if slippage is not None and transactions is not None:
turnover = pos.get_turnover(transactions, positions, period=None)
# Multiply average long/short turnover by two to get the total
# value of buys and sells each day.
turnover *= 2
unadjusted_returns = returns.copy()
returns = txn.adjust_returns_for_slippage(returns, turnover, slippage)
else:
unadjusted_returns = None

create_returns_tear_sheet(
returns,
live_start_date=live_start_date,
Expand All @@ -131,6 +150,7 @@ def create_full_tear_sheet(returns, positions=None, transactions=None,

if transactions is not None:
create_txn_tear_sheet(returns, positions, transactions,
unadjusted_returns=unadjusted_returns,
set_context=set_context)

if bayesian:
Expand Down Expand Up @@ -374,8 +394,8 @@ def create_position_tear_sheet(returns, positions, gross_lev=None,


@plotting_context
def create_txn_tear_sheet(
returns, positions, transactions, return_fig=False):
def create_txn_tear_sheet(returns, positions, transactions,
unadjusted_returns=None, return_fig=False):
"""
Generate a number of plots for analyzing a strategy's transactions.
Expand All @@ -397,9 +417,10 @@ def create_txn_tear_sheet(
set_context : boolean, optional
If True, set default plotting style context.
"""
vertical_sections = 5 if unadjusted_returns is not None else 3

fig = plt.figure(figsize=(14, 3 * 6))
gs = gridspec.GridSpec(3, 3, wspace=0.5, hspace=0.5)
fig = plt.figure(figsize=(14, vertical_sections * 6))
gs = gridspec.GridSpec(vertical_sections, 3, wspace=0.5, hspace=0.5)
ax_turnover = plt.subplot(gs[0, :])
ax_daily_volume = plt.subplot(gs[1, :], sharex=ax_turnover)
ax_turnover_hist = plt.subplot(gs[2, :])
Expand All @@ -418,6 +439,20 @@ def create_txn_tear_sheet(
except AttributeError:
warnings.warn('Unable to generate turnover plot.', UserWarning)

if unadjusted_returns is not None:
ax_slippage_sweep = plt.subplot(gs[3, :], sharex=ax_turnover)
plotting.plot_slippage_sweep(unadjusted_returns,
transactions,
positions,
ax=ax_slippage_sweep
)
ax_slippage_sensitivity = plt.subplot(gs[4, :])
plotting.plot_slippage_sensitivity(unadjusted_returns,
transactions,
positions,
ax=ax_slippage_sensitivity
)

plt.show()
if return_fig:
return fig
Expand Down
25 changes: 25 additions & 0 deletions pyfolio/txn.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,31 @@ def get_txn_vol(transactions):
return pd.concat([daily_values, daily_amounts], axis=1)


def adjust_returns_for_slippage(returns, turnover, slippage_bps):
"""Apply a slippage penalty for every dollar traded.
Parameters
----------
returns : pd.Series
Time series of daily returns.
turnover: pd.Series
Time series of daily total of buys and sells
divided by portfolio value.
- See pos.get_turnover.
slippage_bps: int/float
Basis points of slippage to apply.
Returns
-------
pd.Series
Time series of daily returns, adjusted for slippage.
"""
slippage = 0.0001 * slippage_bps
# Only include returns in the period where the algo traded.
trim_returns = returns.loc[turnover.index]
return trim_returns - turnover * slippage


def create_txn_profits(transactions):
"""
Compute per-trade profits.
Expand Down

0 comments on commit 485e8ff

Please sign in to comment.