Skip to content

Commit

Permalink
STY moved get_turnover to txn.py. Added parameter to set average vs. …
Browse files Browse the repository at this point in the history
…total turnover.
  • Loading branch information
a-campbell committed Oct 16, 2015
1 parent 485e8ff commit 42a5309
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 119 deletions.
48 changes: 20 additions & 28 deletions pyfolio/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -1065,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 @@ -1090,7 +1090,8 @@ def plot_turnover(returns, transactions, positions,


def plot_slippage_sweep(returns, transactions, positions,
slippage_params=None, ax=None, **kwargs):
slippage_params=(3, 8, 10, 12, 15, 20, 50),
ax=None, **kwargs):
"""Plots a equity curves at different per-dollar slippage assumptions.
Parameters
Expand All @@ -1101,10 +1102,10 @@ def plot_slippage_sweep(returns, transactions, positions,
transactions : pd.DataFrame
Daily transaction volume and dollar ammount.
- See full explanation in tears.create_full_tear_sheet.
positions : pd.DataFrame
positions : pd.DataFrame
Daily net position values.
- See full explanation in tears.create_full_tear_sheet.
slippage_params: list
slippage_params: tuple
Slippage pameters to apply to the return time series (in
basis points).
ax : matplotlib.Axes, optional
Expand All @@ -1121,13 +1122,8 @@ def plot_slippage_sweep(returns, transactions, positions,
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]
turnover = txn.get_turnover(transactions, positions,
period='D', average=False)

slippage_sweep = pd.DataFrame()
for bps in slippage_params:
Expand All @@ -1138,8 +1134,7 @@ def plot_slippage_sweep(returns, transactions, positions,
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
Expand All @@ -1157,9 +1152,9 @@ def plot_slippage_sensitivity(returns, transactions, positions,
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).
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
Expand All @@ -1174,24 +1169,21 @@ def plot_slippage_sensitivity(returns, transactions, positions,
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
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.sharpe_ratio(
adj_returns, returns_style='calendar')
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 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)')
ax.set(title='Average Annual Returns Given Per-Dollar Slippage Assumption',
xticks=np.arange(0, 100, 10),
ylabel='Average Annual Return',
xlabel='Per-Dollar Slippage (bps)')

return ax

Expand Down Expand Up @@ -1222,7 +1214,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
8 changes: 3 additions & 5 deletions pyfolio/tears.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,8 @@ def create_full_tear_sheet(returns, positions=None, transactions=None,
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
turnover = txn.get_turnover(transactions, positions,
period=None, average=False)
unadjusted_returns = returns.copy()
returns = txn.adjust_returns_for_slippage(returns, turnover, slippage)
else:
Expand Down Expand Up @@ -440,7 +438,7 @@ def create_txn_tear_sheet(returns, positions, transactions,
warnings.warn('Unable to generate turnover plot.', UserWarning)

if unadjusted_returns is not None:
ax_slippage_sweep = plt.subplot(gs[3, :], sharex=ax_turnover)
ax_slippage_sweep = plt.subplot(gs[3, :])
plotting.plot_slippage_sweep(unadjusted_returns,
transactions,
positions,
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
81 changes: 81 additions & 0 deletions pyfolio/tests/test_txn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from unittest import TestCase

from pandas import (
Series,
DataFrame,
date_range
)
from pandas.util.testing import (assert_series_equal)

from pyfolio.txn import (get_turnover,
adjust_returns_for_slippage)


class TransactionsTestCase(TestCase):

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)

def test_adjust_returns_for_slippage(self):
dates = date_range(start='2015-01-01', freq='D', periods=20)

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

# 100% total, 50% average daily turnover
transactions = DataFrame([[10.0, 0]]*len(dates),
columns=['txn_volume', 'txn_shares'],
index=dates)
returns = Series([0.05]*len(dates), index=dates)
# 0.001% slippage per dollar traded
slippage_bps = 10
expected = Series([0.049]*len(dates), index=dates)

turnover = get_turnover(transactions, positions, average=False)
result = adjust_returns_for_slippage(returns, turnover, slippage_bps)

assert_series_equal(result, expected)
44 changes: 43 additions & 1 deletion pyfolio/txn.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def adjust_returns_for_slippage(returns, turnover, slippage_bps):
turnover: pd.Series
Time series of daily total of buys and sells
divided by portfolio value.
- See pos.get_turnover.
- See txn.get_turnover.
slippage_bps: int/float
Basis points of slippage to apply.
Expand Down Expand Up @@ -173,3 +173,45 @@ def create_txn_profits(transactions):
profits_dts = pd.DataFrame(txn_descr)

return profits_dts


def get_turnover(transactions, positions, period=None, average=True):
"""
Portfolio Turnover Rate:
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.
average : bool
if True, return the average of purchases and sales divided
by portfolio value. If False, return the sum of
purchases and sales divided by portfolio value.
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 if average else traded_value
turnover_rate = turnover / portfolio_value
return turnover_rate

0 comments on commit 42a5309

Please sign in to comment.