3 – Building a Portfolio Manager with Python: Refactoring and removing stocks

This post is part of a series of posts that we will go from complete scratch and build a portfolio management tool using Python.

In the last post, we refactored the file organization and built a function for adding stocks to the “database”.

Here are the previous posts in case you missed them:

Plan for this post

For this post, we will refactor our database to treat it as orders instead of stocks. For that, we will add an ID, also an operation (‘B’ for buy and ‘S’ for sell), and also add a function to remove old order stocks from our “database” (JSON)

Starting to code…

First, we did some updates to the database_handler.py file. We updated the init function to store the next_order_id.

$ database_handler.py
class databaseHandler:
    def __init__(self, database_name = config.DATABASE_NAME):
        self.database_name = database_name
        self.setup_folders()
        self.load_database()
        self.next_order_id = self.get_last_order_id() + 1

Some other things we need to code are the get_order, remove_stock_order, and get_last_order_id functions. Also, we need to update the add_stock function to become the add_stock_order function to start asking for the operation (buy or sell)

$ database_handler.py
...
# get order by order ID
def get_order(self, id):
        if 'orders' not in self.database_data:
            return

        for order in self.database_data['orders'] :
            if order.get('id') == id:
                return order

        return None

    def add_stock_order(self, op, stock_ticker, quantity, price_per_share, purchase_date):
        stock_entry = {
            'id': self.next_order_id,
            'op': op,
            'stock_ticker': stock_ticker,
            'quantity': quantity,
            'price_per_share': price_per_share,
            'purchase_date': purchase_date
        }

        if 'orders' not in self.database_data:
            self.database_data['orders'] = []
        
        # Add new input to the orders
        self.database_data['orders'].append(stock_entry)

        # Saving in JSON file
        with open(config.DATABASE_NAME, 'w') as file:
            json.dump(self.database_data, file, indent=2)
        
        self.next_order_id += 1
        print("Order details added successfully.")

    def remove_stock_order(self, id):
        if 'orders' not in self.database_data:
            return
        
        # Fetch all orders except the deleted id
        self.database_data['orders'] = [order for order in self.database_data['orders'] if order.get('id') != id]

        # Saving in JSON file
        with open(config.DATABASE_NAME, 'w') as file:
            json.dump(self.database_data, file, indent=2)
        
        self.next_order_id += 1
        print(f'Order {id} removed successfully.')

    def get_database(self):
        return self.database_data

    def get_last_order_id(self):
        last_id = -1
        if 'orders' not in self.database_data:
            # first order added, we return -1
            return last_id
        for item in self.database_data['orders']:
            if item['id'] > last_id:
                last_id = item['id']
        return last_id

Note that we updated the name from portfolio to orders in the JSON database. Because of that, we need to make a small change to the operations_handler. Also, on the sum_portfolio operation, we will separate the Buy orders from the Sell orders, so let’s do a tweak in the function:

$ operations_handler.py
...
    # sums portfolio by operation type
    def sum_portfolio(self, order_type):
        database = self.database_handler.get_database()
        
        total_value = 0.0

        for stock_entry in database["orders"]:
            if 'op' in stock_entry and stock_entry['op'] == order_type:
                quantity = stock_entry["quantity"]
                price_per_share = stock_entry["price_per_share"]
                total_value += quantity * price_per_share
        
        return total_value

Now, from view/ui.py, we can update the menu, the operations menu, and the add_stock functions to accommodate our changes. Finally, we will also add the remove_stock_order option

$ view/ui.py
...
# adds stock order in database
def add_stock_order():
    # Prompt user for operation
    while True:
        op = input("Enter the order operation -- Buy or Sell (B or S): ")
        if op == "B" or op == "S":
            break
        else:
            print("Error: Please enter a valid operation (B or S)")

    # Prompt user for stock details
    stock_ticker = input("Enter the stock ticker: ")

    # Prompt user for quantity with error checking
    while True:
        try:
            quantity = float(input("Enter the quantity bought: "))
            if quantity < 0:
                raise ValueError("Quantity must be a non-negative number.")
            break
        except ValueError as ve:
            print(f"Error: {ve}")

    # Prompt user for price per share with error checking
    while True:
        try:
            price_per_share = float(input("Enter the price per share in dollars: "))
            if price_per_share < 0:
                raise ValueError("Price per share must be a non-negative number.")
            break
        except ValueError as ve:
            print(f"Error: {ve}")

    # Prompt user for purchase date with error checking
    while True:
        try:
            purchase_date_str = input("Enter the purchase date (YYYY-MM-DD): ")
            purchase_date = datetime.strptime(purchase_date_str, '%Y-%m-%d').date()
            break
        except ValueError:
            print("Error: Please enter a valid date in the format YYYY-MM-DD.")

    # Add stock details to JSON file
    return op, stock_ticker, quantity, price_per_share, purchase_date_str

def remove_stock_order(database_handler):
    # Prompt user for operation
    while True:
        try:
            id = int(input("Enter the order id you want to delete: (-1 to exit): "))
            if (id == -1):
                break
            order = database_handler.get_order(id)
            if order != None:
                confirm = input(f'Are you sure you want to delete order {id} ? (y or n): ')
                if (confirm == 'y'):
                    break
            else:
                print("Error: Please enter a valid order id")
        except:
            print("Invalid id number, please try again")

    return id

def operations(database_handler):
    operations_handler = operationsHandler(database_handler)
    while True:
        print("\n--- Operations Menu ---")
        print("1 - Show total spent in the portfolio")
        print("0 - Exit")

        option = input("Choose an option: ")

        try:
            option = int(option)
        except ValueError:
            print("Invalid option. Please enter a number.")
            continue

        if option == 1:
            buy_operations_sum = operations_handler.sum_portfolio(order_type = 'B')
            sell_operations_sum = operations_handler.sum_portfolio(order_type = 'S')
            print(f"Buy operations sum: {buy_operations_sum}")
            print(f"Sell operations sum: {sell_operations_sum}")
        elif option == 0:
            break
        else:
            print("Invalid option. Please try again.")

def menu(database_handler):
    while True:
        print("\n--- Methods Menu ---")
        print("1 - Print DATABASE file content")
        print("2 - Add an order to your portfolio")
        print("3 - Remove an order from your portfolio")
        print("4 - Basic Operations")
        print("0 - Exit")

        option = input("Choose an option: ")

        try:
            option = int(option)
        except ValueError:
            print("Invalid option. Please enter a number.")
            continue

        if option == 1:
            database_handler.print_full_database()
        elif option == 2:
            op, stock_ticker, quantity, price_per_share, purchase_date = add_stock_order()
            database_handler.add_stock_order(op, stock_ticker, quantity, price_per_share, purchase_date)
        elif option == 3:
            database_handler.print_full_database()
            id_to_remove = remove_stock_order(database_handler)
            database_handler.remove_stock_order(id_to_remove)
        elif option == 4:
            operations(database_handler)
        elif option == 0:
            break
        else:
            print("Invalid option. Please try again.")

Ok, that is looking promising! Let’s use the flow to add a few stocks by running python main.py. Notice we added two fictional orders, buying 10 APPL @ 100 and selling 50 NVDA @ 20.

And now let’s check the new database.json file, after our changes:

{
  "orders": [
    {
      "id": 0,
      "op": "B",
      "stock_ticker": "AAPL",
      "quantity": 10.0,
      "price_per_share": 100.0,
      "purchase_date": "2024-12-21"
    },
    {
      "id": 1,
      "op": "S",
      "stock_ticker": "NVDA",
      "quantity": 50.0,
      "price_per_share": 20.0,
      "purchase_date": "2024-12-21"
    }
  ]
}

That is looking good, let’s try to do the sum operation for the portfolio:

Ok, it is looking good. We have a thousand dollars for buying operations, and also a thousand dollars for selling operations, since 100*10 = 1,000 and 50*20 is also 1,000.

Now let’s try to remove the NVDA order, let’s assume it is wrong so we need to remove it:

Oh, now let’s check our database:

{
  "orders": [
    {
      "id": 0,
      "op": "B",
      "stock_ticker": "AAPL",
      "quantity": 10.0,
      "price_per_share": 100.0,
      "purchase_date": "2024-12-21"
    }
  ]
}

That is great, the NVDA order is gone! The remove order function is working!

What if we want to do total calculations for a specific ticker, assuming we have more than one order? Well.. this is what we plan to cover in the next post.

That was it for this post!

Hope you are enjoying this series and let me know in the comments what to cover next!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top