-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Sector Exposure #166
Changes from 2 commits
254cdc5
c29009e
e1a60c9
ddcfb44
15ee58e
eb0a755
456c444
a71b13d
c01cefa
736c469
8d9daaf
479a3ef
0f2d2d2
0c0c3b2
16a559c
70070e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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. | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for getting the index we don't need to convert to cum_rets. And I wonder if we even need that. Does the index of sector_alloc not contain the right xlims? |
||
ax.set_xlim((df_cum_rets.index[0], df_cum_rets.index[-1])) | ||
ax.set_ylabel('Exposure by sector') | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not returning |
||
def plot_return_quantiles(returns, df_weekly, df_monthly, ax=None, **kwargs): | ||
"""Creates a box plot of daily, weekly, and monthly return | ||
distributions. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
) | ||
|
||
|
@@ -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. | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.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))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
warnings.warn(warn_message, UserWarning) | ||
|
||
sector_exp = positions.groupby( | ||
by=symbol_sector_map, axis=1).sum() | ||
|
||
sector_exp['cash'] = cash | ||
|
||
return sector_exp |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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: | ||
|
@@ -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. | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. , 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 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) | ||
|
@@ -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 | ||
|
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 ( | ||
|
@@ -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 | ||
|
@@ -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() | ||
|
@@ -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([ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
conditional?