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