Esempio n. 1
0
    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 test_transact_position_new_position():
    """
    Tests the 'transact_position' method for a transaction
    with a brand new asset and checks that all objects are
    set correctly.
    """
    # Create the PositionHandler, Transaction and
    # carry out a transaction
    ph = PositionHandler()
    asset = 'EQ:AMZN'

    transaction = Transaction(
        asset,
        quantity=100,
        dt=pd.Timestamp('2015-05-06 15:00:00', tz=pytz.UTC),
        price=960.0,
        order_id=123,
        commission=26.83
    )
    ph.transact_position(transaction)

    # Check that the position object is set correctly
    pos = ph.positions[asset]

    assert pos.buy_quantity == 100
    assert pos.sell_quantity == 0
    assert pos.net_quantity == 100
    assert pos.direction == 1
    assert pos.avg_price == 960.2683000000001
Esempio n. 3
0
def test_total_values_for_two_separate_transactions():
    """
    Tests 'total_book_cost', 'total_market_value',
    'total_gain' and 'total_perc_gain' for single
    transactions in two separate assets.
    """
    ph = PositionHandler()

    # Asset 1
    asset1 = Equity('Amazon, Inc.', 'AMZN')
    dt1 = pd.Timestamp('2015-05-06')
    trans_pos_1 = Transaction(asset1.symbol,
                              quantity=75,
                              dt=dt1,
                              price=483.45,
                              order_id=1,
                              commission=15.97)
    ph.transact_position(trans_pos_1)

    # Asset 2
    asset2 = Equity('Microsoft, Inc.', 'MSFT')
    dt2 = pd.Timestamp('2015-05-07')
    trans_pos_2 = Transaction(asset2.symbol,
                              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_book_cost() == 71928.07
    assert ph.total_market_value() == 71903.75
    assert ph.total_unrealised_gain() == -24.31999999999971
    assert ph.total_unrealised_percentage_gain() == -0.03381155646190282
Esempio n. 4
0
def test_check_set_position_new_asset():
    """
    Checks the _check_set_position method when
    a new asset is added to the PositionHandler
    and when it is checked subsequently.
    """
    # Create PositionHandler, Asset and OrderedDict
    # positions list
    ph = PositionHandler()
    asset = Equity('Amazon, Inc.', 'AMZN')
    od = OrderedDict()
    assert ph.positions == od

    # Check that the position is set for new asset
    pos = ph._check_set_position(asset)
    assert pos.asset == asset

    # Check that the OrderedDict is correctly set
    # for new asset
    od[asset] = pos
    assert ph.positions == od

    # Check that it works for a current asset
    pos = ph._check_set_position(asset)
    assert pos.asset == asset
    assert ph.positions == od
Esempio n. 5
0
def test_transact_position_new_position():
    """
    Tests the 'transact_position' method for a transaction
    with a brand new asset and checks that all objects are
    set correctly.
    """
    # Create the PositionHandler, Transaction and
    # carry out a transaction
    ph = PositionHandler()
    asset = Equity('Amazon, Inc.', 'AMZN')
    dt = pd.Timestamp('2015-05-06')
    transaction = Transaction(asset,
                              quantity=100,
                              dt=dt,
                              price=960.0,
                              order_id=123,
                              commission=26.83)
    ph.transact_position(transaction)

    # Check that the position object is set correctly
    pos = ph.positions[asset]
    assert pos.quantity == 100
    assert pos.direction == 1.0
    assert pos.book_cost_pu == 960.2683000000001
    assert pos.book_cost == 96026.83
Esempio n. 6
0
    def __init__(
        self,
        start_k,
        starting_cash = 0.0,
        portfolio_id = None,
        name = None
    ):
        
        self.start_k = start_k
        self.current_k = start_k
        self.starting_cash = starting_cash
        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()
Esempio n. 7
0
def test_transact_position_quantity_zero():
    """
    Tests the 'transact_position' method for a transaction
    with net zero quantity after the transaction to ensure
    deletion of the position.
    """
    # Create the PositionHandler, Transaction and
    # carry out a transaction
    ph = PositionHandler()
    asset = Equity('Amazon, Inc.', 'AMZN')
    dt = pd.Timestamp('2015-05-06')
    transaction_long = Transaction(asset,
                                   quantity=100,
                                   dt=dt,
                                   price=960.0,
                                   order_id=123,
                                   commission=26.83)
    transaction_close = Transaction(asset,
                                    quantity=-100,
                                    dt=dt,
                                    price=980.0,
                                    order_id=234,
                                    commission=18.53)

    # Go long and then close, then check that the
    # positions OrderedDict is empty
    ph.transact_position(transaction_long)
    ph.transact_position(transaction_close)
    od = OrderedDict()
    assert ph.positions == od
Esempio n. 8
0
def test_transact_position_current_position():
    """
    Tests the 'transact_position' method for a transaction
    with a current asset and checks that all objects are
    set correctly.
    """
    # Create the PositionHandler, Transaction and
    # carry out a transaction
    ph = PositionHandler()
    asset = Equity('Amazon, Inc.', 'AMZN')
    dt = pd.Timestamp('2015-05-06')
    transaction_long = Transaction(asset,
                                   quantity=100,
                                   dt=dt,
                                   price=960.0,
                                   order_id=123,
                                   commission=26.83)
    transaction_long_again = Transaction(asset,
                                         quantity=200,
                                         dt=dt,
                                         price=990.0,
                                         order_id=234,
                                         commission=18.53)
    ph.transact_position(transaction_long)
    ph.transact_position(transaction_long_again)

    # Check that the position object is set correctly
    pos = ph.positions[asset]
    assert pos.quantity == 300
    assert pos.direction == 1.0
    assert pos.book_cost_pu == 980.1512000000001
    assert pos.book_cost == 294045.36000000004
Esempio n. 9
0
def test_update_position_for_non_none_values():
    """
    Tests the 'update_position' method for non-None
    values when updating a Position entity.
    """
    ph = PositionHandler()

    # Asset 1
    asset1 = Equity('Amazon, Inc.', 'AMZN')
    dt1 = pd.Timestamp('2015-05-06')
    trans_pos_1 = Transaction(asset1,
                              quantity=75,
                              dt=dt1,
                              price=483.45,
                              order_id=1,
                              commission=13.76)
    ph.transact_position(trans_pos_1)

    # Update values manually
    quantity = 100
    current_price = 504.32
    current_dt = pd.Timestamp('2015-05-07')
    book_cost_pu = 23.65
    ph.update_position(asset1,
                       quantity=quantity,
                       current_price=current_price,
                       current_dt=current_dt,
                       book_cost_pu=book_cost_pu)

    assert ph.positions[asset1].quantity == quantity
    assert ph.positions[asset1].current_price == current_price
    assert ph.positions[asset1].current_dt == current_dt
    assert ph.positions[asset1].book_cost_pu == book_cost_pu
def test_transact_position_quantity_zero():
    """
    Tests the 'transact_position' method for a transaction
    with net zero quantity after the transaction to ensure
    deletion of the position.
    """
    # Create the PositionHandler, Transaction and
    # carry out a transaction
    ph = PositionHandler()
    asset = 'EQ:AMZN'
    dt = pd.Timestamp('2015-05-06 15:00:00', tz=pytz.UTC)
    new_dt = pd.Timestamp('2015-05-06 16:00:00', tz=pytz.UTC)

    transaction_long = Transaction(
        asset,
        quantity=100,
        dt=dt,
        price=960.0,
        order_id=123, commission=26.83
    )
    ph.transact_position(transaction_long)

    transaction_close = Transaction(
        asset,
        quantity=-100,
        dt=new_dt,
        price=980.0,
        order_id=234,
        commission=18.53
    )
    ph.transact_position(transaction_close)

    # Go long and then close, then check that the
    # positions OrderedDict is empty
    assert ph.positions == OrderedDict()
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)
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
Esempio n. 13
0
def test_total_values_for_no_transactions():
    """
    Tests 'total_book_cost', 'total_market_value',
    'total_gain' and 'total_perc_gain' for the case
    of no transactions being carried out.
    """
    ph = PositionHandler()
    assert ph.total_book_cost() == 0.0
    assert ph.total_market_value() == 0.0
    assert ph.total_unrealised_gain() == 0.0
    assert ph.total_unrealised_percentage_gain() == 0.0
def test_transact_position_current_position():
    """
    Tests the 'transact_position' method for a transaction
    with a current asset and checks that all objects are
    set correctly.
    """
    # Create the PositionHandler, Transaction and
    # carry out a transaction
    ph = PositionHandler()
    asset = 'EQ:AMZN'
    dt = pd.Timestamp('2015-05-06 15:00:00', tz=pytz.UTC)
    new_dt = pd.Timestamp('2015-05-06 16:00:00', tz=pytz.UTC)

    transaction_long = Transaction(
        asset,
        quantity=100,
        dt=dt,
        price=960.0,
        order_id=123,
        commission=26.83
    )
    ph.transact_position(transaction_long)

    transaction_long_again = Transaction(
        asset,
        quantity=200,
        dt=new_dt,
        price=990.0,
        order_id=234,
        commission=18.53
    )
    ph.transact_position(transaction_long_again)

    # Check that the position object is set correctly
    pos = ph.positions[asset]

    assert pos.buy_quantity == 300
    assert pos.sell_quantity == 0
    assert pos.net_quantity == 300
    assert pos.direction == 1
    assert np.isclose(pos.avg_price, 980.1512)
Esempio n. 15
0
def test_update_commission():
    """
    Tests the 'update_commission' method to ensure
    commission is correctly set on the Position entities.
    """
    ph = PositionHandler()

    # Asset 1
    asset1 = Equity('Amazon, Inc.', 'AMZN')
    dt1 = pd.Timestamp('2015-05-06')
    trans_pos_1 = Transaction(asset1.symbol,
                              quantity=75,
                              dt=dt1,
                              price=483.45,
                              order_id=1,
                              commission=0.0)
    ph.transact_position(trans_pos_1)
    ph.update_commission(asset1.symbol, 15.97)

    # Asset 2
    asset2 = Equity('Microsoft, Inc.', 'MSFT')
    dt2 = pd.Timestamp('2015-05-07')
    trans_pos_2 = Transaction(asset2.symbol,
                              quantity=250,
                              dt=dt2,
                              price=142.58,
                              order_id=2,
                              commission=0.0)
    ph.transact_position(trans_pos_2)
    ph.update_commission(asset2.symbol, 8.35)

    # Check all total values
    assert ph.total_book_cost() == 71928.07
    assert ph.total_market_value() == 71903.75
    assert ph.total_unrealised_gain() == -24.31999999999971
    assert ph.total_unrealised_percentage_gain() == -0.03381155646190282
Esempio n. 16
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'.
        """
        cash_position = Position(self.cash_position_key,
                                 self.starting_cash,
                                 book_cost_pu=1.0,
                                 current_price=1.0,
                                 current_dt=self.current_dt)
        self.pos_handler.positions[self.cash_position_key] = cash_position

        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 cash_position_key(self):
        """
        Obtain the PositionHandler dictionary key for the default
        currency Cash Asset Position.
        """
        return 'CASH:%s' % self.currency

    @property
    def total_cash(self):
        """
        Obtain the total cash available in the default currency
        within the Portfolio.
        """
        cash_position = self.pos_handler.positions[self.cash_position_key]
        return cash_position.quantity

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

    @property
    def total_non_cash_equity(self):
        """
        Obtain the total market value of the portfolio excluding cash.
        """
        return self.total_equity - self.total_cash

    @property
    def total_non_cash_unrealised_gain(self):
        """
        Calculate the sum of all the positions'
        unrealised gains.
        """
        return sum(pos.unrealised_gain
                   for asset, pos in self.pos_handler.positions.items()
                   if not asset.startswith('CASH'))

    @property
    def total_non_cash_unrealised_percentage_gain(self):
        """
        Calculate the total unrealised percentage gain
        on the positions.
        """
        tbc = self.pos_handler.total_book_cost()
        if tbc == 0.0:
            return 0.0
        return (self.total_non_cash_equity - tbc) / tbc * 100.0

    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)

        cash_position = self.pos_handler.positions[self.cash_position_key]

        new_quantity = cash_position.quantity + amount
        self.pos_handler.update_position(self.cash_position_key,
                                         quantity=new_quantity,
                                         current_dt=self.current_dt)

        self.history.append(
            PortfolioEvent.create_subscription(self.current_dt, amount,
                                               self.total_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.total_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.total_cash:
            raise ValueError('Not enough cash in the portfolio to '
                             'withdraw. %s withdrawal request exceeds '
                             'current portfolio cash balance of %s.' %
                             (amount, self.total_cash))

        cash_position = self.pos_handler.positions[self.cash_position_key]

        new_quantity = cash_position.quantity - amount
        self.pos_handler.update_position(self.cash_position_key,
                                         quantity=new_quantity,
                                         current_dt=self.current_dt)

        self.history.append(
            PortfolioEvent.create_withdrawal(self.current_dt, amount,
                                             self.total_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.total_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.total_cash:
            raise ValueError('Not enough cash in the portfolio to '
                             'carry out transaction. Transaction cost of %s '
                             'exceeds remaining cash of %s.' %
                             (txn_total_cost, self.total_cash))

        self.pos_handler.transact_position(txn)
        cash_position = self.pos_handler.positions[self.cash_position_key]

        new_cash_quantity = cash_position.quantity - txn_total_cost
        self.pos_handler.update_position(self.cash_position_key,
                                         quantity=new_cash_quantity,
                                         current_dt=self.current_dt)

        # 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.total_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.total_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.total_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.total_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 assets.
        """
        holdings = {}
        for asset, pos in self.pos_handler.positions.items():
            if not issubclass(asset.__class__,
                              Asset) and not asset.startswith('CASH'):
                holdings[asset] = {
                    "quantity": pos.quantity,
                    "book_cost": pos.book_cost,
                    "market_value": pos.market_value,
                    "gain": pos.unrealised_gain,
                    "perc_gain": pos.unrealised_percentage_gain
                }
        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.update_position(asset,
                                             current_price=current_price,
                                             current_dt=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"])

    def holdings_to_console(self):
        """
        Output the portfolio holdings information to the console.
        """
        def print_row_divider(repeats, symbol="=", cap="*"):
            """
            Prints a row divider for the table.
            """
            sys.stdout.write("%s%s%s\n" % (cap, symbol * repeats, cap))

        # Sort the assets based on their name, not ticker symbol
        pos_sorted = sorted(self.pos_handler.positions.items(),
                            key=lambda x: x[0])

        # Output the name and ID of the portfolio
        sys.stdout.write(
            string_colour("\nPortfolio Holdings | %s - %s\n\n" %
                          (self.portfolio_id, self.name),
                          colour=CYAN))

        # Create the header row and dividers
        repeats = 99
        print_row_divider(repeats)
        sys.stdout.write("|  Holding | Quantity | Price | Change |"
                         "      Book Cost |   Market Value |     "
                         " Unrealised Gain     | \n")
        print_row_divider(repeats)

        # Create the asset holdings rows for each ticker
        ticker_format = '| {0:>8} | {1:>8d} | {2:>5} | ' \
            '{3:>6} | {4:>14} | {5:>14} |'
        for asset, pos in pos_sorted:
            if asset.startswith('CASH'):
                pos_quantity = 0
                pos_book_cost = pos.market_value
                pos_unrealised_gain = '0.00'
                pos_unrealised_percentage_gain = '0.00%'
            else:
                pos_quantity = int(pos.quantity)
                pos_book_cost = pos.book_cost
                pos_unrealised_gain = "%0.2f" % pos.unrealised_gain
                pos_unrealised_percentage_gain = "%0.2f%%" % pos.unrealised_percentage_gain
            sys.stdout.write(
                ticker_format.format(asset, pos_quantity, "-", "-",
                                     "%0.2f" % pos_book_cost,
                                     "%0.2f" % pos.market_value))
            # Colour the gain as red, green or white depending upon
            # whether it is negative, positive or breakeven
            colour = WHITE
            if pos.unrealised_gain > 0.0:
                colour = GREEN
            elif pos.unrealised_gain < 0.0:
                colour = RED
            gain_str = string_colour(pos_unrealised_gain, colour=colour)
            perc_gain_str = string_colour(pos_unrealised_percentage_gain,
                                          colour=colour)
            sys.stdout.write(" " * (25 - len(gain_str)))
            sys.stdout.write(gain_str)
            sys.stdout.write(" " * (22 - len(perc_gain_str)))
            sys.stdout.write(str(perc_gain_str))
            sys.stdout.write(" |\n")

        # Create the totals row
        print_row_divider(repeats)
        total_format = '| {0:>8} | {1:25} | {2:>14} | {3:>14} |'
        sys.stdout.write(
            total_format.format(
                "Total", " ", "%0.2f" % self.pos_handler.total_book_cost(),
                "%0.2f" % self.pos_handler.total_market_value()))
        # Utilise the correct colour for the totals
        # of gain and percentage gain
        colour = WHITE
        total_gain = self.pos_handler.total_unrealised_gain()
        perc_total_gain = self.pos_handler.total_unrealised_percentage_gain()
        if total_gain > 0.0:
            colour = GREEN
        elif total_gain < 0.0:
            colour = RED
        gain_str = string_colour("%0.2f" % total_gain, colour=colour)
        perc_gain_str = string_colour("%0.2f%%" % perc_total_gain,
                                      colour=colour)
        sys.stdout.write(" " * (25 - len(gain_str)))
        sys.stdout.write(gain_str)
        sys.stdout.write(" " * (22 - len(perc_gain_str)))
        sys.stdout.write(str(perc_gain_str))
        sys.stdout.write(" |\n")
        print_row_divider(repeats)
        sys.stdout.write("\n")
Esempio n. 17
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"])