Skip to content

Commit

Permalink
ENH Map positions to sectors, calculate sector exposures, plot sector…
Browse files Browse the repository at this point in the history
… exposures
  • Loading branch information
a-campbell authored and twiecki committed Oct 14, 2015
1 parent 736c469 commit 8d9daaf
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 4 deletions.
40 changes: 40 additions & 0 deletions pyfolio/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,46 @@ def show_and_plot_top_positions(returns, positions_alloc,
return ax


def plot_sector_allocations(returns, sector_alloc, ax=None, **kwargs):
"""Plots the sector exposures of the portfolio over time.
Parameters
----------
returns : pd.Series
Daily returns of the strategy, noncumulative.
- See full explanation in tears.create_full_tear_sheet.
sector_alloc : pd.DataFrame
Portfolio allocation of positions. See pos.get_sector_alloc.
ax : matplotlib.Axes, optional
Axes upon which to plot.
**kwargs, optional
Passed to plotting function.
Returns
-------
ax : matplotlib.Axes, conditional
The axes that were plotted on.
"""
if ax is None:
ax = plt.gcf()

sector_alloc.plot(title='Sector Allocation Over Time',
alpha=0.4, ax=ax, **kwargs)

box = ax.get_position()
ax.set_position([box.x0, box.y0 + box.height * 0.1,
box.width, box.height * 0.9])

# Put a legend below current axis
ax.legend(
loc='upper center', frameon=True, bbox_to_anchor=(
0.5, -0.14), ncol=5)

df_cum_rets = timeseries.cum_returns(returns, starting_value=1)
ax.set_xlim((df_cum_rets.index[0], df_cum_rets.index[-1]))
ax.set_ylabel('Exposure by sector')


def plot_return_quantiles(returns, df_weekly, df_monthly, ax=None, **kwargs):
"""Creates a box plot of daily, weekly, and monthly return
distributions.
Expand Down
37 changes: 37 additions & 0 deletions pyfolio/pos.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from __future__ import division

import pandas as pd
import numpy as np
import warnings


def get_percent_alloc(values):
Expand Down Expand Up @@ -163,3 +165,38 @@ def get_turnover(transactions, positions, period=None):
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.
Parameters
----------
positions : pd.DataFrame
Contains position values or amounts.
symbol_sector_map : dict or pd.Series
Security identifier to sector mapping.
Security ids as keys/index, sectors as values.
Returns
-------
positions_alloc : pd.DataFrame
Sectors and their allocations.
"""
cash = positions.pop('cash')

unmapped_pos = np.setdiff1d(positions.columns.values,
symbol_sector_map.keys())
if len(unmapped_pos) > 0:
warn_message = """Warning: Symbols {} have no sector mapping.
They will not be included in sector allocations""".format(
" ".join(map(str, unmapped_pos)))
warnings.warn(warn_message, UserWarning)

sector_exp = positions.groupby(
by=symbol_sector_map, axis=1).sum()

sector_exp['cash'] = cash

return sector_exp
22 changes: 19 additions & 3 deletions pyfolio/tears.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def create_full_tear_sheet(returns, positions=None, transactions=None,
benchmark_rets=None,
gross_lev=None,
live_start_date=None, bayesian=False,
sector_mappings=None,
cone_std=1.0, set_context=True):
"""
Generate a number of tear sheets that are useful
Expand Down Expand Up @@ -125,6 +126,7 @@ def create_full_tear_sheet(returns, positions=None, transactions=None,
if positions is not None:
create_position_tear_sheet(returns, positions,
gross_lev=gross_lev,
sector_mappings=sector_mappings,
set_context=set_context)

if transactions is not None:
Expand Down Expand Up @@ -301,7 +303,7 @@ def create_returns_tear_sheet(returns, live_start_date=None,
@plotting_context
def create_position_tear_sheet(returns, positions, gross_lev=None,
show_and_plot_top_pos=2,
return_fig=False):
return_fig=False, sector_mappings=None):
"""
Generate a number of plots for analyzing a
strategy's positions and holdings.
Expand All @@ -328,10 +330,15 @@ def create_position_tear_sheet(returns, positions, gross_lev=None,
If True, returns the figure that was plotted on.
set_context : boolean, optional
If True, set default plotting style context.
sector_mapping: dict or pd.Series.
Security identifier to sector mapping.
Security ids as keys, sectors as values.
"""

fig = plt.figure(figsize=(14, 4 * 6))
gs = gridspec.GridSpec(4, 3, wspace=0.5, hspace=0.5)
vertical_sections = 5 if sector_mappings else 4

fig = plt.figure(figsize=(14, vertical_sections * 6))
gs = gridspec.GridSpec(vertical_sections, 3, wspace=0.5, hspace=0.5)
ax_gross_leverage = plt.subplot(gs[0, :])
ax_exposures = plt.subplot(gs[1, :], sharex=ax_gross_leverage)
ax_top_positions = plt.subplot(gs[2, :], sharex=ax_gross_leverage)
Expand All @@ -352,6 +359,15 @@ def create_position_tear_sheet(returns, positions, gross_lev=None,

plotting.plot_holdings(returns, positions_alloc, ax=ax_holdings)

if sector_mappings is not None:
sector_exposures = pos.get_sector_exposures(positions, sector_mappings)

sector_alloc = pos.get_percent_alloc(sector_exposures)
sector_alloc = sector_alloc.drop('cash', axis='columns')
ax_sector_alloc = plt.subplot(gs[4, :], sharex=ax_gross_leverage)
plotting.plot_sector_allocations(returns, sector_alloc,
ax=ax_sector_alloc)

plt.show()
if return_fig:
return fig
Expand Down
44 changes: 43 additions & 1 deletion pyfolio/tests/test_pos.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from unittest import TestCase
from nose_parameterized import parameterized
from collections import OrderedDict

from pandas import (
Expand All @@ -17,10 +18,13 @@

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


class PositionsTestCase(TestCase):
dates = date_range(start='2015-01-01', freq='D', periods=20)

def test_get_percent_alloc(self):
raw_data = arange(15, dtype=float).reshape(5, 3)
Expand Down Expand Up @@ -121,3 +125,41 @@ def test_get_turnover(self):
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),
{0: 'A', 1: 'B', 2: 'A'},
DataFrame([[4.0, 2.0, 10.0]]*len(dates),
columns=['A', 'B', 'cash'], index=dates),
False),
(DataFrame([[1.0, 2.0, 3.0, 10.0]]*len(dates),
columns=[0, 1, 2, 'cash'], index=dates),
Series(index=[0, 1, 2], data=['A', 'B', 'A']),
DataFrame([[4.0, 2.0, 10.0]]*len(dates),
columns=['A', 'B', 'cash'], index=dates),
False),
(DataFrame([[1.0, 2.0, 3.0, 10.0]]*len(dates),
columns=[0, 1, 2, 'cash'], index=dates),
{0: 'A', 1: 'B'},
DataFrame([[1.0, 2.0, 10.0]]*len(dates),
columns=['A', 'B', 'cash'], index=dates),
True)
])
def test_sector_exposure(self, positions, mapping,
expected_sector_exposure,
warning_expected):
"""
Tests sector exposure mapping and rollup.
"""
with warnings.catch_warnings(record=True) as w:
result_sector_exposure = get_sector_exposures(positions,
mapping)

assert_frame_equal(result_sector_exposure,
expected_sector_exposure)
if warning_expected:
assert len(w) == 1
else:
assert len(w) == 0

0 comments on commit 8d9daaf

Please sign in to comment.