Skip to content

Commit e469e06

Browse files
committed
ENH Plot max and median long/short exposures
1 parent 9cc98eb commit e469e06

File tree

4 files changed

+94
-4
lines changed

4 files changed

+94
-4
lines changed

pyfolio/plotting.py

+32
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,38 @@ def show_and_plot_top_positions(returns, positions_alloc,
919919
return ax
920920

921921

922+
def plot_max_median_position_concentration(positions, ax=None, **kwargs):
923+
"""
924+
Plots the max and median of long and short position concentrations
925+
over the time.
926+
927+
Parameters
928+
----------
929+
positions : pd.DataFrame
930+
The positions that the strategy takes over time.
931+
ax : matplotlib.Axes, optional
932+
Axes upon which to plot.
933+
**kwargs, optional
934+
Passed to plotting function.
935+
Returns
936+
-------
937+
ax : matplotlib.Axes
938+
The axes that were plotted on.
939+
"""
940+
if ax is None:
941+
ax = plt.gcf()
942+
943+
alloc_summary = pos.get_max_median_position_concentration(positions)
944+
colors = ['mediumblue', 'steelblue', 'tomato', 'firebrick']
945+
alloc_summary.plot(linewidth=1, color=colors, alpha=0.6, ax=ax)
946+
947+
ax.legend(loc='center left')
948+
ax.set_ylabel('Exposure')
949+
ax.set_title('Long/Short Max and Median Position Concentration')
950+
951+
return ax
952+
953+
922954
def plot_sector_allocations(returns, sector_alloc, ax=None, **kwargs):
923955
"""Plots the sector exposures of the portfolio over time.
924956

pyfolio/pos.py

+31
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,37 @@ def get_top_long_short_abs(positions, top=10):
9797
return df_top_long, df_top_short, df_top_abs
9898

9999

100+
def get_max_median_position_concentration(positions):
101+
"""
102+
Finds the max and median long and short position concentrations
103+
in each time period specified by the index of positions.
104+
105+
Parameters
106+
----------
107+
positions : pd.DataFrame
108+
The positions that the strategy takes over time.
109+
110+
Returns
111+
-------
112+
pd.DataFrame
113+
Columns are max long, max short, median long, and median short
114+
position concentrations. Rows are timeperiods.
115+
"""
116+
expos = get_percent_alloc(positions)
117+
expos = expos.drop('cash', axis=1)
118+
119+
longs = expos.where(expos.applymap(lambda x: x > 0))
120+
shorts = expos.where(expos.applymap(lambda x: x < 0))
121+
122+
alloc_summary = pd.DataFrame()
123+
alloc_summary.loc[:, 'max_long'] = longs.max(axis=1)
124+
alloc_summary.loc[:, 'median_long'] = longs.median(axis=1)
125+
alloc_summary.loc[:, 'median_short'] = shorts.median(axis=1)
126+
alloc_summary.loc[:, 'max_short'] = shorts.min(axis=1)
127+
128+
return alloc_summary
129+
130+
100131
def extract_pos(positions, cash):
101132
"""Extract position values from backtest object as returned by
102133
get_backtest() on the Quantopian research platform.

pyfolio/tears.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -391,14 +391,15 @@ def create_position_tear_sheet(returns, positions, gross_lev=None,
391391

392392
if hide_positions:
393393
show_and_plot_top_pos = 0
394-
vertical_sections = 5 if sector_mappings is not None else 4
394+
vertical_sections = 6 if sector_mappings is not None else 5
395395

396396
fig = plt.figure(figsize=(14, vertical_sections * 6))
397397
gs = gridspec.GridSpec(vertical_sections, 3, wspace=0.5, hspace=0.5)
398398
ax_gross_leverage = plt.subplot(gs[0, :])
399399
ax_exposures = plt.subplot(gs[1, :], sharex=ax_gross_leverage)
400400
ax_top_positions = plt.subplot(gs[2, :], sharex=ax_gross_leverage)
401-
ax_holdings = plt.subplot(gs[3, :], sharex=ax_gross_leverage)
401+
ax_max_median_pos = plt.subplot(gs[3, :], sharex=ax_gross_leverage)
402+
ax_holdings = plt.subplot(gs[4, :], sharex=ax_gross_leverage)
402403

403404
positions_alloc = pos.get_percent_alloc(positions)
404405

@@ -414,14 +415,17 @@ def create_position_tear_sheet(returns, positions, gross_lev=None,
414415
hide_positions=hide_positions,
415416
ax=ax_top_positions)
416417

418+
plotting.plot_max_median_position_concentration(positions,
419+
ax=ax_max_median_pos)
420+
417421
plotting.plot_holdings(returns, positions_alloc, ax=ax_holdings)
418422

419423
if sector_mappings is not None:
420424
sector_exposures = pos.get_sector_exposures(positions, sector_mappings)
421425
if len(sector_exposures.columns) > 1:
422426
sector_alloc = pos.get_percent_alloc(sector_exposures)
423427
sector_alloc = sector_alloc.drop('cash', axis='columns')
424-
ax_sector_alloc = plt.subplot(gs[4, :], sharex=ax_gross_leverage)
428+
ax_sector_alloc = plt.subplot(gs[5, :], sharex=ax_gross_leverage)
425429
plotting.plot_sector_allocations(returns, sector_alloc,
426430
ax=ax_sector_alloc)
427431

pyfolio/tests/test_pos.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
from numpy import (
1414
arange,
1515
zeros_like,
16+
nan,
1617
)
1718

1819
import warnings
1920

2021
from pyfolio.pos import (get_percent_alloc,
2122
extract_pos,
22-
get_sector_exposures)
23+
get_sector_exposures,
24+
get_max_median_position_concentration)
2325

2426

2527
class PositionsTestCase(TestCase):
@@ -115,3 +117,24 @@ def test_sector_exposure(self, positions, mapping,
115117
self.assertEqual(len(w), 1)
116118
else:
117119
self.assertEqual(len(w), 0)
120+
121+
@parameterized.expand([
122+
(DataFrame([[1.0, 2.0, 3.0, 14.0]]*len(dates),
123+
columns=[0, 1, 2, 'cash'], index=dates),
124+
DataFrame([[0.15, 0.1, nan, nan]]*len(dates),
125+
columns=['max_long', 'median_long',
126+
'median_short', 'max_short'], index=dates)),
127+
(DataFrame([[1.0, -2.0, -13.0, 15.0]]*len(dates),
128+
columns=[0, 1, 2, 'cash'], index=dates),
129+
DataFrame([[1.0, 1.0, -7.5, -13.0]]*len(dates),
130+
columns=['max_long', 'median_long',
131+
'median_short', 'max_short'], index=dates)),
132+
(DataFrame([[nan, 2.0, nan, 8.0]]*len(dates),
133+
columns=[0, 1, 2, 'cash'], index=dates),
134+
DataFrame([[0.2, 0.2, nan, nan]]*len(dates),
135+
columns=['max_long', 'median_long',
136+
'median_short', 'max_short'], index=dates))
137+
])
138+
def test_max_median_exposure(self, positions, expected):
139+
alloc_summary = get_max_median_position_concentration(positions)
140+
assert_frame_equal(expected, alloc_summary)

0 commit comments

Comments
 (0)