| 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 |