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

Slippage sweep #170

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 103 additions & 2 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 @@ -1064,7 +1065,7 @@ def plot_turnover(returns, transactions, positions,
y_axis_formatter = FuncFormatter(utils.one_dec_places)
ax.yaxis.set_major_formatter(FuncFormatter(y_axis_formatter))

df_turnover = pos.get_turnover(transactions, positions)
df_turnover = txn.get_turnover(transactions, positions)
df_turnover_by_month = df_turnover.resample("M")
df_turnover.plot(color='steelblue', alpha=1.0, lw=0.5, ax=ax, **kwargs)
df_turnover_by_month.plot(
Expand All @@ -1088,6 +1089,106 @@ def plot_turnover(returns, transactions, positions,
return ax


def plot_slippage_sweep(returns, transactions, positions,
slippage_params=(3, 8, 10, 12, 15, 20, 50),
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: tuple
Slippage pameters to apply to the return time series (in
basis points).
ax : matplotlib.Axes, optional
Axes upon which to plot.
**kwargs, optional
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see these being passed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Minor formatting issue, the positions param needs to be unindented.

Passed to seaborn plotting function.

Returns
-------
ax : matplotlib.Axes
The axes that were plotted on.

"""
if ax is None:
ax = plt.gca()

turnover = txn.get_turnover(transactions, positions,
period=None, average=False)

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 Additional Per-Dollar Slippage')
ax.set_ylabel('')

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

typo 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.
ax : matplotlib.Axes, optional
Axes upon which to plot.
**kwargs, optional
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't seem to get passed.

Passed to seaborn plotting function.

Returns
-------
ax : matplotlib.Axes
The axes that were plotted on.

"""
if ax is None:
ax = plt.gca()

turnover = txn.get_turnover(transactions, positions,
period=None, average=False)
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.annual_return(
adj_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 Additional Per-Dollar Slippage',
xticks=np.arange(0, 100, 10),
ylabel='Average Annual Return',
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 All @@ -1114,7 +1215,7 @@ def plot_daily_turnover_hist(transactions, positions,

if ax is None:
ax = plt.gca()
turnover = pos.get_turnover(transactions, positions, period=None)
turnover = txn.get_turnover(transactions, positions, period=None)
sns.distplot(turnover, ax=ax, **kwargs)
ax.set_title('Distribution of Daily Turnover Rates')
ax.set_xlabel('Turnover Rate')
Expand Down
34 changes: 0 additions & 34 deletions pyfolio/pos.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,40 +129,6 @@ def extract_pos(positions, cash):
return values


def get_turnover(transactions, positions, period=None):
"""
Portfolio Turnover Rate:
Average value of purchases and sales divided
by the average portfolio value for the period.
If no period is provided the period is one time step.
Parameters
----------
transactions_df : pd.DataFrame
Contains transactions data.
- See full explanation in tears.create_full_tear_sheet
positions : pd.DataFrame
Contains daily position values including cash
- See full explanation in tears.create_full_tear_sheet
period : str, optional
Takes the same arguments as df.resample.
Returns
-------
turnover_rate : pd.Series
timeseries of portfolio turnover rates.
"""

traded_value = transactions.txn_volume
portfolio_value = positions.sum(axis=1)
if period is not None:
traded_value = traded_value.resample(period, how='sum')
portfolio_value = portfolio_value.resample(period, how='mean')
# traded_value contains the summed value from buys and sells;
# this is divided by 2.0 to get the average of the two.
turnover = traded_value / 2.0
turnover_rate = turnover / portfolio_value
return turnover_rate


def get_sector_exposures(positions, symbol_sector_map):
"""
Sum position exposures by sector.
Expand Down
41 changes: 37 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,14 @@ 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 = txn.get_turnover(transactions, positions,
period=None, average=False)
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 +148,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 +392,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 +415,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 +437,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, :])
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
55 changes: 4 additions & 51 deletions pyfolio/tests/test_pos.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@
date_range,
Timestamp
)
from pandas.util.testing import (assert_frame_equal,
assert_series_equal)
from pandas.util.testing import (assert_frame_equal)

from numpy import (
absolute,
arange,
zeros_like,
)

import warnings

from pyfolio.pos import (get_percent_alloc,
extract_pos,
get_turnover,
get_sector_exposures)
import warnings


class PositionsTestCase(TestCase):
Expand Down Expand Up @@ -79,53 +79,6 @@ def test_extract_pos(self):

assert_frame_equal(result, expected)

def test_get_turnover(self):
"""
Tests turnover using a 20 day period.

With no transactions the turnover should be 0.

with 100% of the porfolio value traded each day
the daily turnover rate should be 0.5.

For monthly turnover it should be the sum
of the daily turnovers because 20 days < 1 month.

e.g (20 days) * (0.5 daily turn) = 10x monthly turnover rate.
"""
dates = date_range(start='2015-01-01', freq='D', periods=20)

positions = DataFrame([[0.0, 10.0]]*len(dates),
columns=[0, 'cash'], index=dates)
transactions = DataFrame([[0, 0]]*len(dates),
columns=['txn_volume', 'txn_shares'],
index=dates)

# Test with no transactions
expected = Series([0.0]*len(dates), index=dates)
result = get_turnover(transactions, positions)
assert_series_equal(result, expected)

# Monthly freq
index = date_range('01-01-2015', freq='M', periods=1)
expected = Series([0.0], index=index)
result = get_turnover(transactions, positions, period='M')
assert_series_equal(result, expected)

# Test with 0.5 daily turnover
transactions = DataFrame([[10.0, 0]]*len(dates),
columns=['txn_volume', 'txn_shares'],
index=dates)

expected = Series([0.5]*len(dates), index=dates)
result = get_turnover(transactions, positions)
assert_series_equal(result, expected)

# Monthly freq: should be the sum of the daily freq
result = get_turnover(transactions, positions, period='M')
expected = Series([10.0], index=index)
assert_series_equal(result, expected)

@parameterized.expand([
(DataFrame([[1.0, 2.0, 3.0, 10.0]]*len(dates),
columns=[0, 1, 2, 'cash'], index=dates),
Expand Down
Loading