예제 #1
0
    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)
예제 #2
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)))
예제 #3
0
    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)))
예제 #4
0
def test_subscribe_funds_behaviour():
    """
    Test subscribe_funds raises for incorrect datetime
    Test subscribe_funds raises for negative amount
    Test subscribe_funds correctly adds positive
    amount, generates correct event and modifies time
    """
    start_dt = pd.Timestamp('2017-10-05 08:00:00', tz=pytz.UTC)
    earlier_dt = pd.Timestamp('2017-10-04 08:00:00', tz=pytz.UTC)
    later_dt = pd.Timestamp('2017-10-06 08:00:00', tz=pytz.UTC)
    pos_cash = 1000.0
    neg_cash = -1000.0
    port = Portfolio(start_dt, starting_cash=2000.0)

    # Test subscribe_funds raises for incorrect datetime
    with pytest.raises(ValueError):
        port.subscribe_funds(earlier_dt, pos_cash)

    # Test subscribe_funds raises for negative amount
    with pytest.raises(ValueError):
        port.subscribe_funds(start_dt, neg_cash)

    # Test subscribe_funds correctly adds positive
    # amount, generates correct event and modifies time
    port.subscribe_funds(later_dt, pos_cash)

    assert port.cash == 3000.0
    assert port.total_market_value == 0.0
    assert port.total_equity == 3000.0

    pe1 = PortfolioEvent(dt=start_dt,
                         type='subscription',
                         description="SUBSCRIPTION",
                         debit=0.0,
                         credit=2000.0,
                         balance=2000.0)
    pe2 = PortfolioEvent(dt=later_dt,
                         type='subscription',
                         description="SUBSCRIPTION",
                         debit=0.0,
                         credit=1000.0,
                         balance=3000.0)

    assert port.history == [pe1, pe2]
    assert port.current_dt == later_dt
예제 #5
0
    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)))
예제 #6
0
    def _initialise_portfolio_with_cash(self):

        self.cash = copy.copy(self.starting_cash)

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

        self.logger.info(
            '(%s) Funds subscribed to portfolio "%s" '
            '- Credit: %0.2f, Balance: %0.2f' % (
                self.current_k,
                # self.current_k.strftime(settings.LOGGING["DATE_FORMAT"]),
                self.portfolio_id,
                round(self.starting_cash, 2),
                round(self.starting_cash, 2)
            )
        )
예제 #7
0
    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)))
예제 #8
0
def test_transact_asset_behaviour():
    """
    Test transact_asset raises for incorrect time
    Test transact_asset raises for transaction total
    cost exceeding total cash
    Test correct total_cash and total_securities_value
    for correct transaction (commission etc), correct
    portfolio event and correct time update
    """
    start_dt = pd.Timestamp('2017-10-05 08:00:00', tz=pytz.UTC)
    earlier_dt = pd.Timestamp('2017-10-04 08:00:00', tz=pytz.UTC)
    later_dt = pd.Timestamp('2017-10-06 08:00:00', tz=pytz.UTC)
    even_later_dt = pd.Timestamp('2017-10-07 08:00:00', tz=pytz.UTC)
    port = Portfolio(start_dt)
    asset = 'EQ:AAA'

    # Test transact_asset raises for incorrect time
    tn_early = Transaction(asset=asset,
                           quantity=100,
                           dt=earlier_dt,
                           price=567.0,
                           order_id=1,
                           commission=0.0)
    with pytest.raises(ValueError):
        port.transact_asset(tn_early)

    # Test transact_asset raises for transaction total
    # cost exceeding total cash
    port.subscribe_funds(later_dt, 1000.0)

    assert port.cash == 1000.0
    assert port.total_market_value == 0.0
    assert port.total_equity == 1000.0

    pe_sub1 = PortfolioEvent(dt=later_dt,
                             type='subscription',
                             description="SUBSCRIPTION",
                             debit=0.0,
                             credit=1000.0,
                             balance=1000.0)
    tn_large = Transaction(asset=asset,
                           quantity=100,
                           dt=later_dt,
                           price=567.0,
                           order_id=1,
                           commission=15.78)
    with pytest.raises(ValueError):
        port.transact_asset(tn_large)

    # Test correct total_cash and total_securities_value
    # for correct transaction (commission etc), correct
    # portfolio event and correct time update
    port.subscribe_funds(even_later_dt, 99000.0)

    assert port.cash == 100000.0
    assert port.total_market_value == 0.0
    assert port.total_equity == 100000.0

    pe_sub2 = PortfolioEvent(dt=even_later_dt,
                             type='subscription',
                             description="SUBSCRIPTION",
                             debit=0.0,
                             credit=99000.0,
                             balance=100000.0)
    tn_even_later = Transaction(asset=asset,
                                quantity=100,
                                dt=even_later_dt,
                                price=567.0,
                                order_id=1,
                                commission=15.78)
    port.transact_asset(tn_even_later)

    assert port.cash == 43284.22
    assert port.total_market_value == 56700.00
    assert port.total_equity == 99984.22

    description = "LONG 100 EQ:AAA 567.00 07/10/2017"
    pe_tn = PortfolioEvent(dt=even_later_dt,
                           type="asset_transaction",
                           description=description,
                           debit=56715.78,
                           credit=0.0,
                           balance=43284.22)

    assert port.history == [pe_sub1, pe_sub2, pe_tn]
    assert port.current_dt == even_later_dt
예제 #9
0
def test_withdraw_funds_behaviour():
    """
    Test withdraw_funds raises for incorrect datetime
    Test withdraw_funds raises for negative amount
    Test withdraw_funds raises for lack of cash
    Test withdraw_funds correctly subtracts positive
    amount, generates correct event and modifies time
    """
    start_dt = pd.Timestamp('2017-10-05 08:00:00', tz=pytz.UTC)
    earlier_dt = pd.Timestamp('2017-10-04 08:00:00', tz=pytz.UTC)
    later_dt = pd.Timestamp('2017-10-06 08:00:00', tz=pytz.UTC)
    even_later_dt = pd.Timestamp('2017-10-07 08:00:00', tz=pytz.UTC)
    pos_cash = 1000.0
    neg_cash = -1000.0
    port_raise = Portfolio(start_dt)

    # Test withdraw_funds raises for incorrect datetime
    with pytest.raises(ValueError):
        port_raise.withdraw_funds(earlier_dt, pos_cash)

    # Test withdraw_funds raises for negative amount
    with pytest.raises(ValueError):
        port_raise.withdraw_funds(start_dt, neg_cash)

    # Test withdraw_funds raises for not enough cash
    port_broke = Portfolio(start_dt)
    port_broke.subscribe_funds(later_dt, 1000.0)

    with pytest.raises(ValueError):
        port_broke.withdraw_funds(later_dt, 2000.0)

    # Test withdraw_funds correctly subtracts positive
    # amount, generates correct event and modifies time
    # Initial subscribe
    port_cor = Portfolio(start_dt)
    port_cor.subscribe_funds(later_dt, pos_cash)
    pe_sub = PortfolioEvent(dt=later_dt,
                            type='subscription',
                            description="SUBSCRIPTION",
                            debit=0.0,
                            credit=1000.0,
                            balance=1000.0)
    assert port_cor.cash == 1000.0
    assert port_cor.total_market_value == 0.0
    assert port_cor.total_equity == 1000.0
    assert port_cor.history == [pe_sub]
    assert port_cor.current_dt == later_dt

    # Now withdraw
    port_cor.withdraw_funds(even_later_dt, 468.0)
    pe_wdr = PortfolioEvent(dt=even_later_dt,
                            type='withdrawal',
                            description="WITHDRAWAL",
                            debit=468.0,
                            credit=0.0,
                            balance=532.0)
    assert port_cor.cash == 532.0
    assert port_cor.total_market_value == 0.0
    assert port_cor.total_equity == 532.0
    assert port_cor.history == [pe_sub, pe_wdr]
    assert port_cor.current_dt == even_later_dt
예제 #10
0
    def transact_stock(self, txn):
  
        if txn.dt < self.current_k:
            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)