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

Sector Exposure #166

Closed
wants to merge 16 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
108 changes: 76 additions & 32 deletions pyfolio/examples/zipline_algo_example.ipynb

Large diffs are not rendered by default.

45 changes: 43 additions & 2 deletions pyfolio/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ def plot_exposures(returns, positions_alloc, ax=None, **kwargs):
- See full explanation in tears.create_full_tear_sheet.
positions_alloc : pd.DataFrame
Portfolio allocation of positions. See
pos.get_portfolio_alloc.
pos.get_percent_alloc.
ax : matplotlib.Axes, optional
Axes upon which to plot.
**kwargs, optional
Expand Down Expand Up @@ -847,7 +847,7 @@ def show_and_plot_top_positions(returns, positions_alloc,
Daily returns of the strategy, noncumulative.
- See full explanation in tears.create_full_tear_sheet.
positions_alloc : pd.DataFrame
Portfolio allocation of positions. See pos.get_portfolio_alloc.
Portfolio allocation of positions. See pos.get_percent_alloc.
show_and_plot : int, optional
By default, this is 2, and both prints and plots.
If this is 0, it will only plot; if 1, it will only print.
Expand Down Expand Up @@ -921,6 +921,47 @@ 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
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)

ax.set_xlim((sector_alloc.index[0], sector_alloc.index[-1]))
ax.set_ylabel('Exposure by sector')

return ax


Copy link
Contributor

Choose a reason for hiding this comment

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

It's not returning ax.

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
61 changes: 56 additions & 5 deletions pyfolio/pos.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,26 @@
from __future__ import division

import pandas as pd
import numpy as np
import warnings


def get_portfolio_alloc(positions):
def get_percent_alloc(values):
"""
Determines a portfolio's allocations.

Parameters
----------
positions : pd.DataFrame
values : pd.DataFrame
Contains position values or amounts.

Returns
-------
positions_alloc : pd.DataFrame
allocations : pd.DataFrame
Positions and their allocations.
"""
return positions.divide(
positions.abs().sum(axis='columns'),
return values.divide(
values.abs().sum(axis='columns'),
axis='rows'
)

Expand Down Expand Up @@ -163,3 +165,52 @@ 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.
- Example
index 'AAPL' 'MSFT' 'CHK' cash
2004-01-09 13939.380 -15012.993 -403.870 1477.483
2004-01-12 14492.630 -18624.870 142.630 3989.610
2004-01-13 -13853.280 13653.640 -100.980 100.000
symbol_sector_map : dict or pd.Series
Security identifier to sector mapping.
Security ids as keys/index, sectors as values.
- Example:
{'AAPL' : 'Technology'
'MSFT' : 'Technology'
'CHK' : 'Natural Resources'}
Returns
-------
sector_exp : pd.DataFrame
Sectors and their allocations.
- Example:
index 'Technology' 'Natural Resources' cash
2004-01-09 -1073.613 -403.870 1477.4830
2004-01-12 -4132.240 142.630 3989.6100
2004-01-13 -199.640 -100.980 100.0000
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

Adding a small example self-contained example would be helpful here I think.

cash = positions['cash']
positions = positions.drop('cash', axis=1)

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
24 changes: 20 additions & 4 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,16 +330,21 @@ 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, optional
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 is not None 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)
ax_holdings = plt.subplot(gs[3, :], sharex=ax_gross_leverage)

positions_alloc = pos.get_portfolio_alloc(positions)
positions_alloc = pos.get_percent_alloc(positions)

if gross_lev is not None:
plotting.plot_gross_leverage(returns, gross_lev, ax=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
50 changes: 46 additions & 4 deletions 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 @@ -15,14 +16,17 @@
zeros_like,
)

from pyfolio.pos import (get_portfolio_alloc,
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_portfolio_alloc(self):
def test_get_percent_alloc(self):
raw_data = arange(15, dtype=float).reshape(5, 3)
# Make the first column negative to test absolute magnitudes.
raw_data[:, 0] *= -1
Expand All @@ -33,7 +37,7 @@ def test_get_portfolio_alloc(self):
columns=['A', 'B', 'C']
)

result = get_portfolio_alloc(frame)
result = get_percent_alloc(frame)
expected_raw = zeros_like(raw_data)
for idx, row in enumerate(raw_data):
expected_raw[idx] = row / absolute(row).sum()
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([
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

(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