| Total Complexity | 54 |
| Total Lines | 397 |
| Duplicated Lines | 0 % |
Complex classes like zipline.finance.performance.PerformancePeriod 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 | # |
||
| 133 | class PerformancePeriod(object): |
||
| 134 | |||
| 135 | def __init__( |
||
| 136 | self, |
||
| 137 | starting_cash, |
||
| 138 | asset_finder, |
||
| 139 | period_open=None, |
||
| 140 | period_close=None, |
||
| 141 | keep_transactions=True, |
||
| 142 | keep_orders=False, |
||
| 143 | serialize_positions=True): |
||
| 144 | |||
| 145 | self.asset_finder = asset_finder |
||
| 146 | |||
| 147 | self.period_open = period_open |
||
| 148 | self.period_close = period_close |
||
| 149 | |||
| 150 | self.ending_value = 0.0 |
||
| 151 | self.ending_exposure = 0.0 |
||
| 152 | self.period_cash_flow = 0.0 |
||
| 153 | self.pnl = 0.0 |
||
| 154 | |||
| 155 | self.ending_cash = starting_cash |
||
| 156 | |||
| 157 | # Keyed by asset, the previous last sale price of positions with |
||
| 158 | # payouts on price differences, e.g. Futures. |
||
| 159 | # |
||
| 160 | # This dt is not the previous minute to the minute for which the |
||
| 161 | # calculation is done, but the last sale price either before the period |
||
| 162 | # start, or when the price at execution. |
||
| 163 | self._payout_last_sale_prices = {} |
||
| 164 | |||
| 165 | # rollover initializes a number of self's attributes: |
||
| 166 | self.rollover() |
||
| 167 | self.keep_transactions = keep_transactions |
||
| 168 | self.keep_orders = keep_orders |
||
| 169 | |||
| 170 | # An object to recycle via assigning new values |
||
| 171 | # when returning portfolio information. |
||
| 172 | # So as not to avoid creating a new object for each event |
||
| 173 | self._portfolio_store = zp.Portfolio() |
||
| 174 | self._account_store = zp.Account() |
||
| 175 | self.serialize_positions = serialize_positions |
||
| 176 | |||
| 177 | # This dict contains the known cash flow multipliers for sids and is |
||
| 178 | # keyed on sid |
||
| 179 | self._execution_cash_flow_multipliers = {} |
||
| 180 | |||
| 181 | _position_tracker = None |
||
| 182 | |||
| 183 | @property |
||
| 184 | def position_tracker(self): |
||
| 185 | return self._position_tracker |
||
| 186 | |||
| 187 | @position_tracker.setter |
||
| 188 | def position_tracker(self, obj): |
||
| 189 | if obj is None: |
||
| 190 | raise ValueError("position_tracker can not be None") |
||
| 191 | self._position_tracker = obj |
||
| 192 | # we only calculate perf once we inject PositionTracker |
||
| 193 | self.calculate_performance() |
||
| 194 | |||
| 195 | def rollover(self): |
||
| 196 | self.starting_value = self.ending_value |
||
| 197 | self.starting_exposure = self.ending_exposure |
||
| 198 | self.starting_cash = self.ending_cash |
||
| 199 | self.period_cash_flow = 0.0 |
||
| 200 | self.pnl = 0.0 |
||
| 201 | self.processed_transactions = {} |
||
| 202 | self.orders_by_modified = {} |
||
| 203 | self.orders_by_id = OrderedDict() |
||
| 204 | |||
| 205 | payout_assets = self._payout_last_sale_prices.keys() |
||
| 206 | |||
| 207 | for asset in payout_assets: |
||
| 208 | if asset in self._payout_last_sale_prices: |
||
| 209 | self._payout_last_sale_prices[asset] = \ |
||
| 210 | self.position_tracker.positions[asset].last_sale_price |
||
| 211 | else: |
||
| 212 | del self._payout_last_sale_prices[asset] |
||
| 213 | |||
| 214 | def handle_dividends_paid(self, net_cash_payment): |
||
| 215 | if net_cash_payment: |
||
| 216 | self.handle_cash_payment(net_cash_payment) |
||
| 217 | self.calculate_performance() |
||
| 218 | |||
| 219 | def handle_cash_payment(self, payment_amount): |
||
| 220 | self.adjust_cash(payment_amount) |
||
| 221 | |||
| 222 | def handle_commission(self, cost): |
||
| 223 | # Deduct from our total cash pool. |
||
| 224 | self.adjust_cash(-cost) |
||
| 225 | |||
| 226 | def adjust_cash(self, amount): |
||
| 227 | self.period_cash_flow += amount |
||
| 228 | |||
| 229 | def adjust_field(self, field, value): |
||
| 230 | setattr(self, field, value) |
||
| 231 | |||
| 232 | def _get_payout_total(self, positions): |
||
| 233 | payouts = [] |
||
| 234 | for asset, old_price in iteritems(self._payout_last_sale_prices): |
||
| 235 | pos = positions[asset] |
||
| 236 | amount = pos.amount |
||
| 237 | payout = calc_payout( |
||
| 238 | asset.contract_multiplier, |
||
| 239 | amount, |
||
| 240 | old_price, |
||
| 241 | pos.last_sale_price) |
||
| 242 | payouts.append(payout) |
||
| 243 | |||
| 244 | return sum(payouts) |
||
| 245 | |||
| 246 | def calculate_performance(self): |
||
| 247 | pt = self.position_tracker |
||
| 248 | pos_stats = pt.stats() |
||
| 249 | self.ending_value = pos_stats.net_value |
||
| 250 | self.ending_exposure = pos_stats.net_exposure |
||
| 251 | |||
| 252 | payout = self._get_payout_total(pt.positions) |
||
| 253 | |||
| 254 | total_at_start = self.starting_cash + self.starting_value |
||
| 255 | self.ending_cash = self.starting_cash + self.period_cash_flow + payout |
||
| 256 | total_at_end = self.ending_cash + self.ending_value |
||
| 257 | |||
| 258 | self.pnl = total_at_end - total_at_start |
||
| 259 | if total_at_start != 0: |
||
| 260 | self.returns = self.pnl / total_at_start |
||
| 261 | else: |
||
| 262 | self.returns = 0.0 |
||
| 263 | |||
| 264 | def record_order(self, order): |
||
| 265 | if self.keep_orders: |
||
| 266 | try: |
||
| 267 | dt_orders = self.orders_by_modified[order.dt] |
||
| 268 | if order.id in dt_orders: |
||
| 269 | del dt_orders[order.id] |
||
| 270 | except KeyError: |
||
| 271 | self.orders_by_modified[order.dt] = dt_orders = OrderedDict() |
||
| 272 | dt_orders[order.id] = order |
||
| 273 | # to preserve the order of the orders by modified date |
||
| 274 | # we delete and add back. (ordered dictionary is sorted by |
||
| 275 | # first insertion date). |
||
| 276 | if order.id in self.orders_by_id: |
||
| 277 | del self.orders_by_id[order.id] |
||
| 278 | self.orders_by_id[order.id] = order |
||
| 279 | |||
| 280 | def handle_execution(self, txn): |
||
| 281 | self.period_cash_flow += self._calculate_execution_cash_flow(txn) |
||
| 282 | |||
| 283 | asset = self.asset_finder.retrieve_asset(txn.sid) |
||
| 284 | if isinstance(asset, Future): |
||
| 285 | try: |
||
| 286 | old_price = self._payout_last_sale_prices[asset] |
||
| 287 | pos = self.position_tracker.positions[asset] |
||
| 288 | amount = pos.amount |
||
| 289 | price = txn.price |
||
| 290 | cash_adj = calc_payout( |
||
| 291 | asset.contract_multiplier, amount, old_price, price) |
||
| 292 | self.adjust_cash(cash_adj) |
||
| 293 | if amount + txn.amount == 0: |
||
| 294 | del self._payout_last_sale_prices[asset] |
||
| 295 | else: |
||
| 296 | self._payout_last_sale_prices[asset] = price |
||
| 297 | except KeyError: |
||
| 298 | self._payout_last_sale_prices[asset] = txn.price |
||
| 299 | |||
| 300 | if self.keep_transactions: |
||
| 301 | try: |
||
| 302 | self.processed_transactions[txn.dt].append(txn) |
||
| 303 | except KeyError: |
||
| 304 | self.processed_transactions[txn.dt] = [txn] |
||
| 305 | |||
| 306 | def _calculate_execution_cash_flow(self, txn): |
||
| 307 | """ |
||
| 308 | Calculates the cash flow from executing the given transaction |
||
| 309 | """ |
||
| 310 | # Check if the multiplier is cached. If it is not, look up the asset |
||
| 311 | # and cache the multiplier. |
||
| 312 | try: |
||
| 313 | multiplier = self._execution_cash_flow_multipliers[txn.sid] |
||
| 314 | except KeyError: |
||
| 315 | asset = self.asset_finder.retrieve_asset(txn.sid) |
||
| 316 | # Futures experience no cash flow on transactions |
||
| 317 | if isinstance(asset, Future): |
||
| 318 | multiplier = 0 |
||
| 319 | else: |
||
| 320 | multiplier = 1 |
||
| 321 | self._execution_cash_flow_multipliers[txn.sid] = multiplier |
||
| 322 | |||
| 323 | # Calculate and return the cash flow given the multiplier |
||
| 324 | return -1 * txn.price * txn.amount * multiplier |
||
| 325 | |||
| 326 | # backwards compat. TODO: remove? |
||
| 327 | @property |
||
| 328 | def positions(self): |
||
| 329 | return self.position_tracker.positions |
||
| 330 | |||
| 331 | @property |
||
| 332 | def position_amounts(self): |
||
| 333 | return self.position_tracker.position_amounts |
||
| 334 | |||
| 335 | def __core_dict(self): |
||
| 336 | pos_stats = self.position_tracker.stats() |
||
| 337 | period_stats = calc_period_stats(pos_stats, self.ending_cash) |
||
| 338 | |||
| 339 | rval = { |
||
| 340 | 'ending_value': self.ending_value, |
||
| 341 | 'ending_exposure': self.ending_exposure, |
||
| 342 | # this field is renamed to capital_used for backward |
||
| 343 | # compatibility. |
||
| 344 | 'capital_used': self.period_cash_flow, |
||
| 345 | 'starting_value': self.starting_value, |
||
| 346 | 'starting_exposure': self.starting_exposure, |
||
| 347 | 'starting_cash': self.starting_cash, |
||
| 348 | 'ending_cash': self.ending_cash, |
||
| 349 | 'portfolio_value': self.ending_cash + self.ending_value, |
||
| 350 | 'pnl': self.pnl, |
||
| 351 | 'returns': self.returns, |
||
| 352 | 'period_open': self.period_open, |
||
| 353 | 'period_close': self.period_close, |
||
| 354 | 'gross_leverage': period_stats.gross_leverage, |
||
| 355 | 'net_leverage': period_stats.net_leverage, |
||
| 356 | 'short_exposure': pos_stats.short_exposure, |
||
| 357 | 'long_exposure': pos_stats.long_exposure, |
||
| 358 | 'short_value': pos_stats.short_value, |
||
| 359 | 'long_value': pos_stats.long_value, |
||
| 360 | 'longs_count': pos_stats.longs_count, |
||
| 361 | 'shorts_count': pos_stats.shorts_count, |
||
| 362 | } |
||
| 363 | |||
| 364 | return rval |
||
| 365 | |||
| 366 | def to_dict(self, dt=None): |
||
| 367 | """ |
||
| 368 | Creates a dictionary representing the state of this performance |
||
| 369 | period. See header comments for a detailed description. |
||
| 370 | |||
| 371 | Kwargs: |
||
| 372 | dt (datetime): If present, only return transactions for the dt. |
||
| 373 | """ |
||
| 374 | rval = self.__core_dict() |
||
| 375 | |||
| 376 | if self.serialize_positions: |
||
| 377 | positions = self.position_tracker.get_positions_list() |
||
| 378 | rval['positions'] = positions |
||
| 379 | |||
| 380 | # we want the key to be absent, not just empty |
||
| 381 | if self.keep_transactions: |
||
| 382 | if dt: |
||
| 383 | # Only include transactions for given dt |
||
| 384 | try: |
||
| 385 | transactions = [x.to_dict() |
||
| 386 | for x in self.processed_transactions[dt]] |
||
| 387 | except KeyError: |
||
| 388 | transactions = [] |
||
| 389 | else: |
||
| 390 | transactions = \ |
||
| 391 | [y.to_dict() |
||
| 392 | for x in itervalues(self.processed_transactions) |
||
| 393 | for y in x] |
||
| 394 | rval['transactions'] = transactions |
||
| 395 | |||
| 396 | if self.keep_orders: |
||
| 397 | if dt: |
||
| 398 | # only include orders modified as of the given dt. |
||
| 399 | try: |
||
| 400 | orders = [x.to_dict() |
||
| 401 | for x in itervalues(self.orders_by_modified[dt])] |
||
| 402 | except KeyError: |
||
| 403 | orders = [] |
||
| 404 | else: |
||
| 405 | orders = [x.to_dict() for x in itervalues(self.orders_by_id)] |
||
| 406 | rval['orders'] = orders |
||
| 407 | |||
| 408 | return rval |
||
| 409 | |||
| 410 | def as_portfolio(self): |
||
| 411 | """ |
||
| 412 | The purpose of this method is to provide a portfolio |
||
| 413 | object to algorithms running inside the same trading |
||
| 414 | client. The data needed is captured raw in a |
||
| 415 | PerformancePeriod, and in this method we rename some |
||
| 416 | fields for usability and remove extraneous fields. |
||
| 417 | """ |
||
| 418 | # Recycles containing objects' Portfolio object |
||
| 419 | # which is used for returning values. |
||
| 420 | # as_portfolio is called in an inner loop, |
||
| 421 | # so repeated object creation becomes too expensive |
||
| 422 | portfolio = self._portfolio_store |
||
| 423 | # maintaining the old name for the portfolio field for |
||
| 424 | # backward compatibility |
||
| 425 | portfolio.capital_used = self.period_cash_flow |
||
| 426 | portfolio.starting_cash = self.starting_cash |
||
| 427 | portfolio.portfolio_value = self.ending_cash + self.ending_value |
||
| 428 | portfolio.pnl = self.pnl |
||
| 429 | portfolio.returns = self.returns |
||
| 430 | portfolio.cash = self.ending_cash |
||
| 431 | portfolio.start_date = self.period_open |
||
| 432 | portfolio.positions = self.position_tracker.get_positions() |
||
| 433 | portfolio.positions_value = self.ending_value |
||
| 434 | portfolio.positions_exposure = self.ending_exposure |
||
| 435 | return portfolio |
||
| 436 | |||
| 437 | def as_account(self): |
||
| 438 | account = self._account_store |
||
| 439 | |||
| 440 | pt = self.position_tracker |
||
| 441 | pos_stats = pt.stats() |
||
| 442 | period_stats = calc_period_stats(pos_stats, self.ending_cash) |
||
| 443 | |||
| 444 | # If no attribute is found on the PerformancePeriod resort to the |
||
| 445 | # following default values. If an attribute is found use the existing |
||
| 446 | # value. For instance, a broker may provide updates to these |
||
| 447 | # attributes. In this case we do not want to over write the broker |
||
| 448 | # values with the default values. |
||
| 449 | account.settled_cash = \ |
||
| 450 | getattr(self, 'settled_cash', self.ending_cash) |
||
| 451 | account.accrued_interest = \ |
||
| 452 | getattr(self, 'accrued_interest', 0.0) |
||
| 453 | account.buying_power = \ |
||
| 454 | getattr(self, 'buying_power', float('inf')) |
||
| 455 | account.equity_with_loan = \ |
||
| 456 | getattr(self, 'equity_with_loan', |
||
| 457 | self.ending_cash + self.ending_value) |
||
| 458 | account.total_positions_value = \ |
||
| 459 | getattr(self, 'total_positions_value', self.ending_value) |
||
| 460 | account.total_positions_value = \ |
||
| 461 | getattr(self, 'total_positions_exposure', self.ending_exposure) |
||
| 462 | account.regt_equity = \ |
||
| 463 | getattr(self, 'regt_equity', self.ending_cash) |
||
| 464 | account.regt_margin = \ |
||
| 465 | getattr(self, 'regt_margin', float('inf')) |
||
| 466 | account.initial_margin_requirement = \ |
||
| 467 | getattr(self, 'initial_margin_requirement', 0.0) |
||
| 468 | account.maintenance_margin_requirement = \ |
||
| 469 | getattr(self, 'maintenance_margin_requirement', 0.0) |
||
| 470 | account.available_funds = \ |
||
| 471 | getattr(self, 'available_funds', self.ending_cash) |
||
| 472 | account.excess_liquidity = \ |
||
| 473 | getattr(self, 'excess_liquidity', self.ending_cash) |
||
| 474 | account.cushion = \ |
||
| 475 | getattr(self, 'cushion', |
||
| 476 | self.ending_cash / (self.ending_cash + self.ending_value)) |
||
| 477 | account.day_trades_remaining = \ |
||
| 478 | getattr(self, 'day_trades_remaining', float('inf')) |
||
| 479 | account.leverage = getattr(self, 'leverage', |
||
| 480 | period_stats.gross_leverage) |
||
| 481 | account.net_leverage = period_stats.net_leverage |
||
| 482 | |||
| 483 | account.net_liquidation = getattr(self, 'net_liquidation', |
||
| 484 | period_stats.net_liquidation) |
||
| 485 | return account |
||
| 486 | |||
| 487 | def __getstate__(self): |
||
| 488 | state_dict = {k: v for k, v in iteritems(self.__dict__) |
||
| 489 | if not k.startswith('_')} |
||
| 490 | |||
| 491 | state_dict['_portfolio_store'] = self._portfolio_store |
||
| 492 | state_dict['_account_store'] = self._account_store |
||
| 493 | |||
| 494 | state_dict['processed_transactions'] = \ |
||
| 495 | dict(self.processed_transactions) |
||
| 496 | state_dict['orders_by_id'] = \ |
||
| 497 | dict(self.orders_by_id) |
||
| 498 | state_dict['orders_by_modified'] = \ |
||
| 499 | dict(self.orders_by_modified) |
||
| 500 | state_dict['_payout_last_sale_prices'] = \ |
||
| 501 | self._payout_last_sale_prices |
||
| 502 | |||
| 503 | STATE_VERSION = 3 |
||
| 504 | state_dict[VERSION_LABEL] = STATE_VERSION |
||
| 505 | return state_dict |
||
| 506 | |||
| 507 | def __setstate__(self, state): |
||
| 508 | |||
| 509 | OLDEST_SUPPORTED_STATE = 3 |
||
| 510 | version = state.pop(VERSION_LABEL) |
||
| 511 | |||
| 512 | if version < OLDEST_SUPPORTED_STATE: |
||
| 513 | raise BaseException("PerformancePeriod saved state is too old.") |
||
| 514 | |||
| 515 | processed_transactions = {} |
||
| 516 | processed_transactions.update(state.pop('processed_transactions')) |
||
| 517 | |||
| 518 | orders_by_id = OrderedDict() |
||
| 519 | orders_by_id.update(state.pop('orders_by_id')) |
||
| 520 | |||
| 521 | orders_by_modified = {} |
||
| 522 | orders_by_modified.update(state.pop('orders_by_modified')) |
||
| 523 | self.processed_transactions = processed_transactions |
||
| 524 | self.orders_by_id = orders_by_id |
||
| 525 | self.orders_by_modified = orders_by_modified |
||
| 526 | |||
| 527 | self._execution_cash_flow_multipliers = {} |
||
| 528 | |||
| 529 | self.__dict__.update(state) |
||
| 530 |