Skip to content

Commit a9e6a71

Browse files
authored
Add tilt and timing performance attribution. Also respect time range (#111)
* ENH Add tilt and timing performance attribution. Also respect time ranges.
1 parent ad10570 commit a9e6a71

File tree

2 files changed

+69
-8
lines changed

2 files changed

+69
-8
lines changed

empyrical/perf_attrib.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import OrderedDict
12
import pandas as pd
23

34

@@ -81,17 +82,36 @@ def perf_attrib(returns,
8182
----
8283
See https://en.wikipedia.org/wiki/Performance_attribution for more details.
8384
"""
85+
86+
# Make risk data match time range of returns
87+
start = returns.index[0]
88+
end = returns.index[-1]
89+
factor_returns = factor_returns.loc[start:end]
90+
factor_loadings = factor_loadings.loc[start:end]
91+
92+
factor_loadings.index = factor_loadings.index.set_names(['dt', 'ticker'])
93+
94+
positions = positions.copy()
95+
positions.index = positions.index.set_names(['dt', 'ticker'])
96+
8497
risk_exposures_portfolio = compute_exposures(positions,
8598
factor_loadings)
8699

87100
perf_attrib_by_factor = risk_exposures_portfolio.multiply(factor_returns)
88-
89101
common_returns = perf_attrib_by_factor.sum(axis='columns')
102+
103+
tilt_exposure = risk_exposures_portfolio.mean()
104+
tilt_returns = factor_returns.multiply(tilt_exposure).sum(axis='columns')
105+
timing_returns = common_returns - tilt_returns
90106
specific_returns = returns - common_returns
91107

92-
returns_df = pd.DataFrame({'total_returns': returns,
93-
'common_returns': common_returns,
94-
'specific_returns': specific_returns})
108+
returns_df = pd.DataFrame(OrderedDict([
109+
('total_returns', returns),
110+
('common_returns', common_returns),
111+
('specific_returns', specific_returns),
112+
('tilt_returns', tilt_returns),
113+
('timing_returns', timing_returns)
114+
]))
95115

96116
return (risk_exposures_portfolio,
97117
pd.concat([perf_attrib_by_factor, returns_df], axis='columns'))

empyrical/tests/test_perf_attrib.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,15 @@ def test_perf_attrib_simple(self):
4242

4343
expected_perf_attrib_output = pd.DataFrame(
4444
index=dts,
45-
columns=['risk_factor1', 'risk_factor2', 'common_returns',
46-
'specific_returns', 'total_returns'],
45+
columns=['risk_factor1', 'risk_factor2', 'total_returns',
46+
'common_returns', 'specific_returns',
47+
'tilt_returns', 'timing_returns'],
4748
data={'risk_factor1': [0.025, 0.025],
4849
'risk_factor2': [0.025, 0.025],
4950
'common_returns': [0.05, 0.05],
5051
'specific_returns': [0.05, 0.05],
52+
'tilt_returns': [0.05, 0.05],
53+
'timing_returns': [0.0, 0.0],
5154
'total_returns': returns}
5255
)
5356

@@ -79,12 +82,15 @@ def test_perf_attrib_simple(self):
7982

8083
expected_perf_attrib_output = pd.DataFrame(
8184
index=dts,
82-
columns=['risk_factor1', 'risk_factor2', 'common_returns',
83-
'specific_returns', 'total_returns'],
85+
columns=['risk_factor1', 'risk_factor2', 'total_returns',
86+
'common_returns', 'specific_returns',
87+
'tilt_returns', 'timing_returns'],
8488
data={'risk_factor1': [0.0, 0.0],
8589
'risk_factor2': [0.0, 0.0],
8690
'common_returns': [0.0, 0.0],
8791
'specific_returns': [0.1, 0.1],
92+
'tilt_returns': [0.0, 0.0],
93+
'timing_returns': [0.0, 0.0],
8894
'total_returns': returns}
8995
)
9096

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

110+
# test long and short positions with tilt exposure
111+
positions = pd.Series([1.0, -0.5, 1.0, -0.5], index=index)
112+
113+
exposures_portfolio, perf_attrib_output = perf_attrib(returns,
114+
positions,
115+
factor_returns,
116+
factor_loadings)
117+
118+
expected_perf_attrib_output = pd.DataFrame(
119+
index=dts,
120+
columns=['risk_factor1', 'risk_factor2', 'total_returns',
121+
'common_returns', 'specific_returns',
122+
'tilt_returns', 'timing_returns'],
123+
data={'risk_factor1': [0.0125, 0.0125],
124+
'risk_factor2': [0.0125, 0.0125],
125+
'common_returns': [0.025, 0.025],
126+
'specific_returns': [0.075, 0.075],
127+
'tilt_returns': [0.025, 0.025],
128+
'timing_returns': [0.0, 0.0],
129+
'total_returns': returns}
130+
)
131+
132+
expected_exposures_portfolio = pd.DataFrame(
133+
index=dts,
134+
columns=['risk_factor1', 'risk_factor2'],
135+
data={'risk_factor1': [0.125, 0.125],
136+
'risk_factor2': [0.125, 0.125]}
137+
)
138+
139+
pd.util.testing.assert_frame_equal(expected_perf_attrib_output,
140+
perf_attrib_output)
141+
142+
pd.util.testing.assert_frame_equal(expected_exposures_portfolio,
143+
exposures_portfolio)
144+
104145
def test_perf_attrib_regression(self):
105146

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

0 commit comments

Comments
 (0)