Skip to content

Commit

Permalink
Merge branch 'slippage-sweep'
Browse files Browse the repository at this point in the history
  • Loading branch information
twiecki committed Oct 21, 2015
2 parents 2d4b934 + 4baf9c4 commit 07e40a5
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 91 deletions.
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 @@ -1076,7 +1077,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 @@ -1100,6 +1101,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
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.
- 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
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 @@ -1126,7 +1227,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, 1.5, 2.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 @@ -112,6 +121,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 @@ -375,8 +393,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 @@ -398,9 +416,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 @@ -419,6 +438,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

0 comments on commit 07e40a5

Please sign in to comment.