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
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
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
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
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()
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
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
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
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)
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
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")
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"])