Skip to content

Commit

Permalink
Add tilt and timing performance attribution. Also respect time range (#…
Browse files Browse the repository at this point in the history
…111)

* ENH Add tilt and timing performance attribution. Also respect time ranges.
  • Loading branch information
twiecki authored Aug 15, 2019
1 parent ad10570 commit a9e6a71
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 8 deletions.
28 changes: 24 additions & 4 deletions empyrical/perf_attrib.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import OrderedDict
import pandas as pd


Expand Down Expand Up @@ -81,17 +82,36 @@ def perf_attrib(returns,
----
See https://en.wikipedia.org/wiki/Performance_attribution for more details.
"""

# Make risk data match time range of returns
start = returns.index[0]
end = returns.index[-1]
factor_returns = factor_returns.loc[start:end]
factor_loadings = factor_loadings.loc[start:end]

factor_loadings.index = factor_loadings.index.set_names(['dt', 'ticker'])

positions = positions.copy()
positions.index = positions.index.set_names(['dt', 'ticker'])

risk_exposures_portfolio = compute_exposures(positions,
factor_loadings)

perf_attrib_by_factor = risk_exposures_portfolio.multiply(factor_returns)

common_returns = perf_attrib_by_factor.sum(axis='columns')

tilt_exposure = risk_exposures_portfolio.mean()
tilt_returns = factor_returns.multiply(tilt_exposure).sum(axis='columns')
timing_returns = common_returns - tilt_returns
specific_returns = returns - common_returns

returns_df = pd.DataFrame({'total_returns': returns,
'common_returns': common_returns,
'specific_returns': specific_returns})
returns_df = pd.DataFrame(OrderedDict([
('total_returns', returns),
('common_returns', common_returns),
('specific_returns', specific_returns),
('tilt_returns', tilt_returns),
('timing_returns', timing_returns)
]))

return (risk_exposures_portfolio,
pd.concat([perf_attrib_by_factor, returns_df], axis='columns'))
Expand Down
49 changes: 45 additions & 4 deletions empyrical/tests/test_perf_attrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@ def test_perf_attrib_simple(self):

expected_perf_attrib_output = pd.DataFrame(
index=dts,
columns=['risk_factor1', 'risk_factor2', 'common_returns',
'specific_returns', 'total_returns'],
columns=['risk_factor1', 'risk_factor2', 'total_returns',
'common_returns', 'specific_returns',
'tilt_returns', 'timing_returns'],
data={'risk_factor1': [0.025, 0.025],
'risk_factor2': [0.025, 0.025],
'common_returns': [0.05, 0.05],
'specific_returns': [0.05, 0.05],
'tilt_returns': [0.05, 0.05],
'timing_returns': [0.0, 0.0],
'total_returns': returns}
)

Expand Down Expand Up @@ -79,12 +82,15 @@ def test_perf_attrib_simple(self):

expected_perf_attrib_output = pd.DataFrame(
index=dts,
columns=['risk_factor1', 'risk_factor2', 'common_returns',
'specific_returns', 'total_returns'],
columns=['risk_factor1', 'risk_factor2', 'total_returns',
'common_returns', 'specific_returns',
'tilt_returns', 'timing_returns'],
data={'risk_factor1': [0.0, 0.0],
'risk_factor2': [0.0, 0.0],
'common_returns': [0.0, 0.0],
'specific_returns': [0.1, 0.1],
'tilt_returns': [0.0, 0.0],
'timing_returns': [0.0, 0.0],
'total_returns': returns}
)

Expand All @@ -101,6 +107,41 @@ def test_perf_attrib_simple(self):
pd.util.testing.assert_frame_equal(expected_exposures_portfolio,
exposures_portfolio)

# test long and short positions with tilt exposure
positions = pd.Series([1.0, -0.5, 1.0, -0.5], index=index)

exposures_portfolio, perf_attrib_output = perf_attrib(returns,
positions,
factor_returns,
factor_loadings)

expected_perf_attrib_output = pd.DataFrame(
index=dts,
columns=['risk_factor1', 'risk_factor2', 'total_returns',
'common_returns', 'specific_returns',
'tilt_returns', 'timing_returns'],
data={'risk_factor1': [0.0125, 0.0125],
'risk_factor2': [0.0125, 0.0125],
'common_returns': [0.025, 0.025],
'specific_returns': [0.075, 0.075],
'tilt_returns': [0.025, 0.025],
'timing_returns': [0.0, 0.0],
'total_returns': returns}
)

expected_exposures_portfolio = pd.DataFrame(
index=dts,
columns=['risk_factor1', 'risk_factor2'],
data={'risk_factor1': [0.125, 0.125],
'risk_factor2': [0.125, 0.125]}
)

pd.util.testing.assert_frame_equal(expected_perf_attrib_output,
perf_attrib_output)

pd.util.testing.assert_frame_equal(expected_exposures_portfolio,
exposures_portfolio)

def test_perf_attrib_regression(self):

positions = pd.read_csv('empyrical/tests/test_data/positions.csv',
Expand Down

0 comments on commit a9e6a71

Please sign in to comment.