| Total Complexity | 40 |
| Total Lines | 299 |
| Duplicated Lines | 0 % |
Complex classes like zipline.finance.risk.RiskMetricsPeriod often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | # |
||
| 47 | class RiskMetricsPeriod(object): |
||
| 48 | def __init__(self, start_date, end_date, returns, env, |
||
| 49 | benchmark_returns=None, algorithm_leverages=None): |
||
| 50 | |||
| 51 | self.env = env |
||
| 52 | treasury_curves = env.treasury_curves |
||
| 53 | if treasury_curves.index[-1] >= start_date: |
||
| 54 | mask = ((treasury_curves.index >= start_date) & |
||
| 55 | (treasury_curves.index <= end_date)) |
||
| 56 | |||
| 57 | self.treasury_curves = treasury_curves[mask] |
||
| 58 | else: |
||
| 59 | # our test is beyond the treasury curve history |
||
| 60 | # so we'll use the last available treasury curve |
||
| 61 | self.treasury_curves = treasury_curves[-1:] |
||
| 62 | |||
| 63 | self.start_date = start_date |
||
| 64 | self.end_date = end_date |
||
| 65 | |||
| 66 | if benchmark_returns is None: |
||
| 67 | br = env.benchmark_returns |
||
| 68 | benchmark_returns = br[(br.index >= returns.index[0]) & |
||
| 69 | (br.index <= returns.index[-1])] |
||
| 70 | |||
| 71 | self.algorithm_returns = self.mask_returns_to_period(returns, |
||
| 72 | env) |
||
| 73 | self.benchmark_returns = self.mask_returns_to_period(benchmark_returns, |
||
| 74 | env) |
||
| 75 | self.algorithm_leverages = algorithm_leverages |
||
| 76 | |||
| 77 | self.calculate_metrics() |
||
| 78 | |||
| 79 | def calculate_metrics(self): |
||
| 80 | |||
| 81 | self.benchmark_period_returns = \ |
||
| 82 | self.calculate_period_returns(self.benchmark_returns) |
||
| 83 | |||
| 84 | self.algorithm_period_returns = \ |
||
| 85 | self.calculate_period_returns(self.algorithm_returns) |
||
| 86 | |||
| 87 | if not self.algorithm_returns.index.equals( |
||
| 88 | self.benchmark_returns.index |
||
| 89 | ): |
||
| 90 | message = "Mismatch between benchmark_returns ({bm_count}) and \ |
||
| 91 | algorithm_returns ({algo_count}) in range {start} : {end}" |
||
| 92 | message = message.format( |
||
| 93 | bm_count=len(self.benchmark_returns), |
||
| 94 | algo_count=len(self.algorithm_returns), |
||
| 95 | start=self.start_date, |
||
| 96 | end=self.end_date |
||
| 97 | ) |
||
| 98 | raise Exception(message) |
||
| 99 | |||
| 100 | self.num_trading_days = len(self.benchmark_returns) |
||
| 101 | self.trading_day_counts = pd.stats.moments.rolling_count( |
||
| 102 | self.algorithm_returns, self.num_trading_days) |
||
| 103 | |||
| 104 | self.mean_algorithm_returns = \ |
||
| 105 | self.algorithm_returns.cumsum() / self.trading_day_counts |
||
| 106 | |||
| 107 | self.benchmark_volatility = self.calculate_volatility( |
||
| 108 | self.benchmark_returns) |
||
| 109 | self.algorithm_volatility = self.calculate_volatility( |
||
| 110 | self.algorithm_returns) |
||
| 111 | self.treasury_period_return = choose_treasury( |
||
| 112 | self.treasury_curves, |
||
| 113 | self.start_date, |
||
| 114 | self.end_date, |
||
| 115 | self.env, |
||
| 116 | ) |
||
| 117 | self.sharpe = self.calculate_sharpe() |
||
| 118 | # The consumer currently expects a 0.0 value for sharpe in period, |
||
| 119 | # this differs from cumulative which was np.nan. |
||
| 120 | # When factoring out the sharpe_ratio, the different return types |
||
| 121 | # were collapsed into `np.nan`. |
||
| 122 | # TODO: Either fix consumer to accept `np.nan` or make the |
||
| 123 | # `sharpe_ratio` return type configurable. |
||
| 124 | # In the meantime, convert nan values to 0.0 |
||
| 125 | if pd.isnull(self.sharpe): |
||
| 126 | self.sharpe = 0.0 |
||
| 127 | self.sortino = self.calculate_sortino() |
||
| 128 | self.information = self.calculate_information() |
||
| 129 | self.beta, self.algorithm_covariance, self.benchmark_variance, \ |
||
| 130 | self.condition_number, self.eigen_values = self.calculate_beta() |
||
| 131 | self.alpha = self.calculate_alpha() |
||
| 132 | self.excess_return = self.algorithm_period_returns - \ |
||
| 133 | self.treasury_period_return |
||
| 134 | self.max_drawdown = self.calculate_max_drawdown() |
||
| 135 | self.max_leverage = self.calculate_max_leverage() |
||
| 136 | |||
| 137 | def to_dict(self): |
||
| 138 | """ |
||
| 139 | Creates a dictionary representing the state of the risk report. |
||
| 140 | Returns a dict object of the form: |
||
| 141 | """ |
||
| 142 | period_label = self.end_date.strftime("%Y-%m") |
||
| 143 | rval = { |
||
| 144 | 'trading_days': self.num_trading_days, |
||
| 145 | 'benchmark_volatility': self.benchmark_volatility, |
||
| 146 | 'algo_volatility': self.algorithm_volatility, |
||
| 147 | 'treasury_period_return': self.treasury_period_return, |
||
| 148 | 'algorithm_period_return': self.algorithm_period_returns, |
||
| 149 | 'benchmark_period_return': self.benchmark_period_returns, |
||
| 150 | 'sharpe': self.sharpe, |
||
| 151 | 'sortino': self.sortino, |
||
| 152 | 'information': self.information, |
||
| 153 | 'beta': self.beta, |
||
| 154 | 'alpha': self.alpha, |
||
| 155 | 'excess_return': self.excess_return, |
||
| 156 | 'max_drawdown': self.max_drawdown, |
||
| 157 | 'max_leverage': self.max_leverage, |
||
| 158 | 'period_label': period_label |
||
| 159 | } |
||
| 160 | |||
| 161 | return {k: None if check_entry(k, v) else v |
||
| 162 | for k, v in iteritems(rval)} |
||
| 163 | |||
| 164 | def __repr__(self): |
||
| 165 | statements = [] |
||
| 166 | metrics = [ |
||
| 167 | "algorithm_period_returns", |
||
| 168 | "benchmark_period_returns", |
||
| 169 | "excess_return", |
||
| 170 | "num_trading_days", |
||
| 171 | "benchmark_volatility", |
||
| 172 | "algorithm_volatility", |
||
| 173 | "sharpe", |
||
| 174 | "sortino", |
||
| 175 | "information", |
||
| 176 | "algorithm_covariance", |
||
| 177 | "benchmark_variance", |
||
| 178 | "beta", |
||
| 179 | "alpha", |
||
| 180 | "max_drawdown", |
||
| 181 | "max_leverage", |
||
| 182 | "algorithm_returns", |
||
| 183 | "benchmark_returns", |
||
| 184 | "condition_number", |
||
| 185 | "eigen_values" |
||
| 186 | ] |
||
| 187 | |||
| 188 | for metric in metrics: |
||
| 189 | value = getattr(self, metric) |
||
| 190 | statements.append("{m}:{v}".format(m=metric, v=value)) |
||
| 191 | |||
| 192 | return '\n'.join(statements) |
||
| 193 | |||
| 194 | def mask_returns_to_period(self, daily_returns, env): |
||
| 195 | if isinstance(daily_returns, list): |
||
| 196 | returns = pd.Series([x.returns for x in daily_returns], |
||
| 197 | index=[x.date for x in daily_returns]) |
||
| 198 | else: # otherwise we're receiving an index already |
||
| 199 | returns = daily_returns |
||
| 200 | |||
| 201 | trade_days = env.trading_days |
||
| 202 | trade_day_mask = returns.index.normalize().isin(trade_days) |
||
| 203 | |||
| 204 | mask = ((returns.index >= self.start_date) & |
||
| 205 | (returns.index <= self.end_date) & trade_day_mask) |
||
| 206 | |||
| 207 | returns = returns[mask] |
||
| 208 | return returns |
||
| 209 | |||
| 210 | def calculate_period_returns(self, returns): |
||
| 211 | period_returns = (1. + returns).prod() - 1 |
||
| 212 | return period_returns |
||
| 213 | |||
| 214 | def calculate_volatility(self, daily_returns): |
||
| 215 | return np.std(daily_returns, ddof=1) * math.sqrt(self.num_trading_days) |
||
| 216 | |||
| 217 | def calculate_sharpe(self): |
||
| 218 | """ |
||
| 219 | http://en.wikipedia.org/wiki/Sharpe_ratio |
||
| 220 | """ |
||
| 221 | return sharpe_ratio(self.algorithm_volatility, |
||
| 222 | self.algorithm_period_returns, |
||
| 223 | self.treasury_period_return) |
||
| 224 | |||
| 225 | def calculate_sortino(self): |
||
| 226 | """ |
||
| 227 | http://en.wikipedia.org/wiki/Sortino_ratio |
||
| 228 | """ |
||
| 229 | mar = downside_risk(self.algorithm_returns, |
||
| 230 | self.mean_algorithm_returns, |
||
| 231 | self.num_trading_days) |
||
| 232 | # Hold on to downside risk for debugging purposes. |
||
| 233 | self.downside_risk = mar |
||
| 234 | return sortino_ratio(self.algorithm_period_returns, |
||
| 235 | self.treasury_period_return, |
||
| 236 | mar) |
||
| 237 | |||
| 238 | def calculate_information(self): |
||
| 239 | """ |
||
| 240 | http://en.wikipedia.org/wiki/Information_ratio |
||
| 241 | """ |
||
| 242 | return information_ratio(self.algorithm_returns, |
||
| 243 | self.benchmark_returns) |
||
| 244 | |||
| 245 | def calculate_beta(self): |
||
| 246 | """ |
||
| 247 | |||
| 248 | .. math:: |
||
| 249 | |||
| 250 | \\beta_a = \\frac{\mathrm{Cov}(r_a,r_p)}{\mathrm{Var}(r_p)} |
||
| 251 | |||
| 252 | http://en.wikipedia.org/wiki/Beta_(finance) |
||
| 253 | """ |
||
| 254 | # it doesn't make much sense to calculate beta for less than two days, |
||
| 255 | # so return nan. |
||
| 256 | if len(self.algorithm_returns) < 2: |
||
| 257 | return np.nan, np.nan, np.nan, np.nan, [] |
||
| 258 | |||
| 259 | returns_matrix = np.vstack([self.algorithm_returns, |
||
| 260 | self.benchmark_returns]) |
||
| 261 | C = np.cov(returns_matrix, ddof=1) |
||
| 262 | |||
| 263 | # If there are missing benchmark values, then we can't calculate the |
||
| 264 | # beta. |
||
| 265 | if not np.isfinite(C).all(): |
||
| 266 | return np.nan, np.nan, np.nan, np.nan, [] |
||
| 267 | |||
| 268 | eigen_values = la.eigvals(C) |
||
| 269 | condition_number = max(eigen_values) / min(eigen_values) |
||
| 270 | algorithm_covariance = C[0][1] |
||
| 271 | benchmark_variance = C[1][1] |
||
| 272 | beta = algorithm_covariance / benchmark_variance |
||
| 273 | |||
| 274 | return ( |
||
| 275 | beta, |
||
| 276 | algorithm_covariance, |
||
| 277 | benchmark_variance, |
||
| 278 | condition_number, |
||
| 279 | eigen_values |
||
| 280 | ) |
||
| 281 | |||
| 282 | def calculate_alpha(self): |
||
| 283 | """ |
||
| 284 | http://en.wikipedia.org/wiki/Alpha_(investment) |
||
| 285 | """ |
||
| 286 | return alpha(self.algorithm_period_returns, |
||
| 287 | self.treasury_period_return, |
||
| 288 | self.benchmark_period_returns, |
||
| 289 | self.beta) |
||
| 290 | |||
| 291 | def calculate_max_drawdown(self): |
||
| 292 | compounded_returns = [] |
||
| 293 | cur_return = 0.0 |
||
| 294 | for r in self.algorithm_returns: |
||
| 295 | try: |
||
| 296 | cur_return += math.log(1.0 + r) |
||
| 297 | # this is a guard for a single day returning -100%, if returns are |
||
| 298 | # greater than -1.0 it will throw an error because you cannot take |
||
| 299 | # the log of a negative number |
||
| 300 | except ValueError: |
||
| 301 | log.debug("{cur} return, zeroing the returns".format( |
||
| 302 | cur=cur_return)) |
||
| 303 | cur_return = 0.0 |
||
| 304 | compounded_returns.append(cur_return) |
||
| 305 | |||
| 306 | cur_max = None |
||
| 307 | max_drawdown = None |
||
| 308 | for cur in compounded_returns: |
||
| 309 | if cur_max is None or cur > cur_max: |
||
| 310 | cur_max = cur |
||
| 311 | |||
| 312 | drawdown = (cur - cur_max) |
||
| 313 | if max_drawdown is None or drawdown < max_drawdown: |
||
| 314 | max_drawdown = drawdown |
||
| 315 | |||
| 316 | if max_drawdown is None: |
||
| 317 | return 0.0 |
||
| 318 | |||
| 319 | return 1.0 - math.exp(max_drawdown) |
||
| 320 | |||
| 321 | def calculate_max_leverage(self): |
||
| 322 | if self.algorithm_leverages is None: |
||
| 323 | return 0.0 |
||
| 324 | else: |
||
| 325 | return max(self.algorithm_leverages) |
||
| 326 | |||
| 327 | def __getstate__(self): |
||
| 328 | state_dict = {k: v for k, v in iteritems(self.__dict__) |
||
| 329 | if not k.startswith('_')} |
||
| 330 | |||
| 331 | STATE_VERSION = 3 |
||
| 332 | state_dict[VERSION_LABEL] = STATE_VERSION |
||
| 333 | |||
| 334 | return state_dict |
||
| 335 | |||
| 336 | def __setstate__(self, state): |
||
| 337 | |||
| 338 | OLDEST_SUPPORTED_STATE = 3 |
||
| 339 | version = state.pop(VERSION_LABEL) |
||
| 340 | |||
| 341 | if version < OLDEST_SUPPORTED_STATE: |
||
| 342 | raise BaseException("RiskMetricsPeriod saved state \ |
||
| 343 | is too old.") |
||
| 344 | |||
| 345 | self.__dict__.update(state) |
||
| 346 |