def test_total_values_for_no_transactions():
    """
    Tests 'total_market_value', 'total_unrealised_pnl',
    'total_realised_pnl' and 'total_pnl' for the case
    of no transactions being carried out.
    """
    ph = PositionHandler()
    assert ph.total_market_value() == 0.0
    assert ph.total_unrealised_pnl() == 0.0
    assert ph.total_realised_pnl() == 0.0
    assert ph.total_pnl() == 0.0
def test_total_values_for_two_separate_transactions():
    """
    Tests 'total_market_value', 'total_unrealised_pnl',
    'total_realised_pnl' and 'total_pnl' for single
    transactions in two separate assets.
    """
    ph = PositionHandler()

    # Asset 1
    asset1 = 'EQ:AMZN'
    dt1 = pd.Timestamp('2015-05-06 15:00:00', tz=pytz.UTC)
    trans_pos_1 = Transaction(
        asset1,
        quantity=75,
        dt=dt1,
        price=483.45,
        order_id=1,
        commission=15.97
    )
    ph.transact_position(trans_pos_1)

    # Asset 2
    asset2 = 'EQ:MSFT'
    dt2 = pd.Timestamp('2015-05-07 15:00:00', tz=pytz.UTC)
    trans_pos_2 = Transaction(
        asset2,
        quantity=250,
        dt=dt2,
        price=142.58,
        order_id=2,
        commission=8.35
    )
    ph.transact_position(trans_pos_2)

    # Check all total values
    assert ph.total_market_value() == 71903.75
    assert np.isclose(ph.total_unrealised_pnl(), -24.31999999999971)
    assert ph.total_realised_pnl() == 0.0
    assert np.isclose(ph.total_pnl(), -24.31999999999971)
Example #3
0
class Portfolio(object):
    """
    Represents a portfolio of assets. It contains a cash
    account with the ability to subscribe and withdraw funds.
    It also contains a list of positions in assets, encapsulated
    by a PositionHandler instance.

    Parameters
    ----------
    start_dt : datetime
        Portfolio creation datetime.
    starting_cash : float, optional
        Starting cash of the portfolio. Defaults to 100,000 USD.
    currency: str, optional
        The portfolio denomination currency.
    portfolio_id: str, optional
        An identifier for the portfolio.
    name: str, optional
        The human-readable name of the portfolio.
    """
    def __init__(self,
                 start_dt,
                 starting_cash=0.0,
                 currency="USD",
                 portfolio_id=None,
                 name=None):
        """
        Initialise the Portfolio object with a PositionHandler,
        an event history, along with cash balance. Make sure
        the portfolio denomination currency is also set.
        """
        self.start_dt = start_dt
        self.current_dt = start_dt
        self.starting_cash = starting_cash
        self.currency = currency
        self.portfolio_id = portfolio_id
        self.name = name

        self.pos_handler = PositionHandler()
        self.history = []

        self.logger = logging.getLogger('Portfolio')
        self.logger.setLevel(logging.DEBUG)
        self.logger.info(
            '(%s) Portfolio "%s" instance initialised' %
            (self.current_dt.strftime(
                settings.LOGGING["DATE_FORMAT"]), self.portfolio_id))

        self._initialise_portfolio_with_cash()

    def _initialise_portfolio_with_cash(self):
        """
        Initialise the portfolio with a (default) currency Cash Asset
        with quantity equal to 'starting_cash'.
        """
        self.cash = copy.copy(self.starting_cash)

        if self.starting_cash > 0.0:
            self.history.append(
                PortfolioEvent.create_subscription(self.current_dt,
                                                   self.starting_cash,
                                                   self.starting_cash))

        self.logger.info(
            '(%s) Funds subscribed to portfolio "%s" '
            '- Credit: %0.2f, Balance: %0.2f' %
            (self.current_dt.strftime(
                settings.LOGGING["DATE_FORMAT"]), self.portfolio_id,
             round(self.starting_cash, 2), round(self.starting_cash, 2)))

    @property
    def total_market_value(self):
        """
        Obtain the total market value of the portfolio excluding cash.
        """
        return self.pos_handler.total_market_value()

    @property
    def total_equity(self):
        """
        Obtain the total market value of the portfolio including cash.
        """
        return self.total_market_value + self.cash

    @property
    def total_unrealised_pnl(self):
        """
        Calculate the sum of all the positions' unrealised P&Ls.
        """
        return self.pos_handler.total_unrealised_pnl()

    @property
    def total_realised_pnl(self):
        """
        Calculate the sum of all the positions' realised P&Ls.
        """
        return self.pos_handler.total_realised_pnl()

    @property
    def total_pnl(self):
        """
        Calculate the sum of all the positions' total P&Ls.
        """
        return self.pos_handler.total_pnl()

    def subscribe_funds(self, dt, amount):
        """
        Credit funds to the portfolio.
        """
        if dt < self.current_dt:
            raise ValueError('Subscription datetime (%s) is earlier than '
                             'current portfolio datetime (%s). Cannot '
                             'subscribe funds.' % (dt, self.current_dt))
        self.current_dt = dt

        if amount < 0.0:
            raise ValueError('Cannot credit negative amount: '
                             '%s to the portfolio.' % amount)

        self.cash += amount

        self.history.append(
            PortfolioEvent.create_subscription(self.current_dt, amount,
                                               self.cash))

        self.logger.info(
            '(%s) Funds subscribed to portfolio "%s" '
            '- Credit: %0.2f, Balance: %0.2f' %
            (self.current_dt.strftime(settings.LOGGING["DATE_FORMAT"]),
             self.portfolio_id, round(amount, 2), round(self.cash, 2)))

    def withdraw_funds(self, dt, amount):
        """
        Withdraw funds from the portfolio if there is enough
        cash to allow it.
        """
        # Check that amount is positive and that there is
        # enough in the portfolio to withdraw the funds
        if dt < self.current_dt:
            raise ValueError('Withdrawal datetime (%s) is earlier than '
                             'current portfolio datetime (%s). Cannot '
                             'withdraw funds.' % (dt, self.current_dt))
        self.current_dt = dt

        if amount < 0:
            raise ValueError('Cannot debit negative amount: '
                             '%0.2f from the portfolio.' % amount)

        if amount > self.cash:
            raise ValueError('Not enough cash in the portfolio to '
                             'withdraw. %s withdrawal request exceeds '
                             'current portfolio cash balance of %s.' %
                             (amount, self.cash))

        self.cash -= amount

        self.history.append(
            PortfolioEvent.create_withdrawal(self.current_dt, amount,
                                             self.cash))

        self.logger.info(
            '(%s) Funds withdrawn from portfolio "%s" '
            '- Debit: %0.2f, Balance: %0.2f' %
            (self.current_dt.strftime(settings.LOGGING["DATE_FORMAT"]),
             self.portfolio_id, round(amount, 2), round(self.cash, 2)))

    def transact_asset(self, txn):
        """
        Adjusts positions to account for a transaction.
        """
        if txn.dt < self.current_dt:
            raise ValueError('Transaction datetime (%s) is earlier than '
                             'current portfolio datetime (%s). Cannot '
                             'transact assets.' % (txn.dt, self.current_dt))
        self.current_dt = txn.dt

        txn_share_cost = txn.price * txn.quantity
        txn_total_cost = txn_share_cost + txn.commission

        if txn_total_cost > self.cash:
            if settings.PRINT_EVENTS:
                print('WARNING: Not enough cash in the portfolio to '
                      'carry out transaction. Transaction cost of %s '
                      'exceeds remaining cash of %s. Transaction '
                      'will proceed with a negative cash balance.' %
                      (txn_total_cost, self.cash))

        self.pos_handler.transact_position(txn)

        self.cash -= txn_total_cost

        # Form Portfolio history details
        direction = "LONG" if txn.direction > 0 else "SHORT"
        description = "%s %s %s %0.2f %s" % (
            direction, txn.quantity, txn.asset.upper(), txn.price,
            datetime.datetime.strftime(txn.dt, "%d/%m/%Y"))
        if direction == "LONG":
            pe = PortfolioEvent(dt=txn.dt,
                                type='asset_transaction',
                                description=description,
                                debit=round(txn_total_cost, 2),
                                credit=0.0,
                                balance=round(self.cash, 2))
            self.logger.info(
                '(%s) Asset "%s" transacted LONG in portfolio "%s" '
                '- Debit: %0.2f, Balance: %0.2f' %
                (txn.dt.strftime(settings.LOGGING["DATE_FORMAT"]), txn.asset,
                 self.portfolio_id, round(txn_total_cost,
                                          2), round(self.cash, 2)))
        else:
            pe = PortfolioEvent(dt=txn.dt,
                                type='asset_transaction',
                                description=description,
                                debit=0.0,
                                credit=-1.0 * round(txn_total_cost, 2),
                                balance=round(self.cash, 2))
            self.logger.info(
                '(%s) Asset "%s" transacted SHORT in portfolio "%s" '
                '- Credit: %0.2f, Balance: %0.2f' %
                (txn.dt.strftime(settings.LOGGING["DATE_FORMAT"]), txn.asset,
                 self.portfolio_id, -1.0 * round(txn_total_cost, 2),
                 round(self.cash, 2)))
        self.history.append(pe)

    def portfolio_to_dict(self):
        """
        Output the portfolio holdings information as a dictionary
        with Assets as keys and sub-dictionaries as values.
        This excludes cash.

        Returns
        -------
        `dict`
            The portfolio holdings.
        """
        holdings = {}
        for asset, pos in self.pos_handler.positions.items():
            holdings[asset] = {
                "quantity": pos.net_quantity,
                "market_value": pos.market_value,
                "unrealised_pnl": pos.unrealised_pnl,
                "realised_pnl": pos.realised_pnl,
                "total_pnl": pos.total_pnl
            }
        return holdings

    def update_market_value_of_asset(self, asset, current_price, current_dt):
        """
        Update the market value of the asset to the current
        trade price and date.
        """
        if asset not in self.pos_handler.positions:
            return
        else:
            if current_price < 0.0:
                raise ValueError('Current trade price of %s is negative for '
                                 'asset %s. Cannot update position.' %
                                 (current_price, asset))

            if current_dt < self.current_dt:
                raise ValueError('Current trade date of %s is earlier than '
                                 'current date %s of asset %s. Cannot update '
                                 'position.' %
                                 (current_dt, self.current_dt, asset))

            self.pos_handler.positions[asset].update_current_price(
                current_price, current_dt)

    def history_to_df(self):
        """
        Creates a Pandas DataFrame of the Portfolio history.
        """
        records = [pe.to_dict() for pe in self.history]
        return pd.DataFrame.from_records(records,
                                         columns=[
                                             "date", "type", "description",
                                             "debit", "credit", "balance"
                                         ]).set_index(keys=["date"])