Problem Set 9 - Finance 💹

Problem Set 9 - Finance 💹

Flask - Week 9 of CS50x 2022 ✨

·

15 min read

In this problem set, we are tasked to build a web application that allows users to buy and sell stocks, using a virtual currency, and track their portfolio over time. We will take a closer look at the various technologies used in the problem set, such as Flask, SQL, HTML, and CSS, as well as integrate external APIs to retrieve real-time stock prices. We will also delve into the different functionalities of the web application and how they are implemented!

To start, we are provided with two Python files and a folder of HTML with Jinja templates.

application.py

import os

from cs50 import SQL
from flask import Flask, flash, redirect, render_template, request, session
from flask_session import Session
from tempfile import mkdtemp
from werkzeug.security import check_password_hash, generate_password_hash

from helpers import apology, login_required, lookup, usd

# Configure application
app = Flask(__name__)

# Ensure templates are auto-reloaded
app.config["TEMPLATES_AUTO_RELOAD"] = True

# Custom filter
app.jinja_env.filters["usd"] = usd

# Configure session to use filesystem (instead of signed cookies)
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)

# Configure CS50 Library to use SQLite database
db = SQL("sqlite:///finance.db")

# Make sure API key is set
if not os.environ.get("API_KEY"):
    raise RuntimeError("API_KEY not set")


@app.after_request
def after_request(response):
    """Ensure responses aren't cached"""
    response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    response.headers["Expires"] = 0
    response.headers["Pragma"] = "no-cache"
    return response


@app.route("/")
@login_required
def index():
    """Show portfolio of stocks"""
    return apology("TODO")


@app.route("/buy", methods=["GET", "POST"])
@login_required
def buy():
    """Buy shares of stock"""
    return apology("TODO")


@app.route("/history")
@login_required
def history():
    """Show history of transactions"""
    return apology("TODO")


@app.route("/login", methods=["GET", "POST"])
def login():
    """Log user in"""

    # Forget any user_id
    session.clear()

    # User reached route via POST (as by submitting a form via POST)
    if request.method == "POST":

        # Ensure username was submitted
        if not request.form.get("username"):
            return apology("must provide username", 403)

        # Ensure password was submitted
        elif not request.form.get("password"):
            return apology("must provide password", 403)

        # Query database for username
        rows = db.execute("SELECT * FROM users WHERE username = ?", request.form.get("username"))

        # Ensure username exists and password is correct
        if len(rows) != 1 or not check_password_hash(rows[0]["hash"], request.form.get("password")):
            return apology("invalid username and/or password", 403)

        # Remember which user has logged in
        session["user_id"] = rows[0]["id"]

        # Redirect user to home page
        return redirect("/")

    # User reached route via GET (as by clicking a link or via redirect)
    else:
        return render_template("login.html")


@app.route("/logout")
def logout():
    """Log user out"""

    # Forget any user_id
    session.clear()

    # Redirect user to login form
    return redirect("/")


@app.route("/quote", methods=["GET", "POST"])
@login_required
def quote():
    """Get stock quote."""
    return apology("TODO")


@app.route("/register", methods=["GET", "POST"])
def register():
    """Register user"""
    return apology("TODO")


@app.route("/sell", methods=["GET", "POST"])
@login_required
def sell():
    """Sell shares of stock"""
    return apology("TODO")

The first Python file we will explore is application.py. This file contains the primary logic for the web application, including the different routes that users can access, such as /register, /quote, /buy, /index, /sell, and /history. A /login route is also provided which handles user authentication. The route ensures that only registered users can access the application, preventing unauthorized access to the user's portfolio and transaction history. By utilizing Flask and the Flask-Session library, the application can securely manage user sessions and prevent session hijacking. This file also uses the CS50 library to interact with an SQLite database that stores user information, transaction history, and stock data. This file serves as the backbone of the web application, providing the necessary infrastructure to handle user requests and responses.

helpers.py

import os
import requests
import urllib.parse

from flask import redirect, render_template, request, session
from functools import wraps


def apology(message, code=400):
    """Render message as an apology to user."""
    def escape(s):
        """
        Escape special characters.

        https://github.com/jacebrowning/memegen#special-characters
        """
        for old, new in [("-", "--"), (" ", "-"), ("_", "__"), ("?", "~q"),
                         ("%", "~p"), ("#", "~h"), ("/", "~s"), ("\"", "''")]:
            s = s.replace(old, new)
        return s
    return render_template("apology.html", top=code, bottom=escape(message)), code


def login_required(f):
    """
    Decorate routes to require login.

    https://flask.palletsprojects.com/en/1.1.x/patterns/viewdecorators/
    """
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if session.get("user_id") is None:
            return redirect("/login")
        return f(*args, **kwargs)
    return decorated_function


def lookup(symbol):
    """Look up quote for symbol."""

    # Contact API
    try:
        api_key = os.environ.get("API_KEY")
        url = f"https://cloud.iexapis.com/stable/stock/{urllib.parse.quote_plus(symbol)}/quote?token={api_key}"
        response = requests.get(url)
        response.raise_for_status()
    except requests.RequestException:
        return None

    # Parse response
    try:
        quote = response.json()
        return {
            "name": quote["companyName"],
            "price": float(quote["latestPrice"]),
            "symbol": quote["symbol"]
        }
    except (KeyError, TypeError, ValueError):
        return None


def usd(value):
    """Format value as USD."""
    return f"${value:,.2f}"

The second Python file we will explore is helpers.py which provides several useful functions to support the application logic. The functions include an apology function to render error messages, a login_required decorator to ensure authentication before accessing certain routes, a lookup function to retrieve the latest stock price for a given symbol using an API, and a usd function to format numbers as USD currency. The functions are imported into the application.py file and used to enhance the functionality of the web application.

HTML with Jinja Templates

<!DOCTYPE html>

<html lang="en">

    <head>

        <meta charset="utf-8">
        <meta name="viewport" content="initial-scale=1, width=device-width">

        <!-- http://getbootstrap.com/docs/5.2/ -->
        <link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" rel="stylesheet">
        <script crossorigin="anonymous" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"></script>

        <!-- https://favicon.io/emoji-favicons/money-bag/ -->
        <link href="/static/favicon.ico" rel="icon">

        <link href="/static/styles.css" rel="stylesheet">

        <title>C$50 Finance: {% block title %}{% endblock %}</title>

    </head>

    <body>

        <nav class="bg-light border navbar navbar-expand-md navbar-light">
            <div class="container-fluid">
                <a class="navbar-brand" href="/"><span class="blue">C</span><span class="red">$</span><span class="yellow">5</span><span class="green">0</span> <span class="red">Finance</span></a>
                <button aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler" data-bs-target="#navbar" data-bs-toggle="collapse" type="button">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbar">
                    {% if session["user_id"] %}
                        <ul class="navbar-nav me-auto mt-2">
                            <li class="nav-item"><a class="nav-link" href="/quote">Quote</a></li>
                            <li class="nav-item"><a class="nav-link" href="/buy">Buy</a></li>
                            <li class="nav-item"><a class="nav-link" href="/sell">Sell</a></li>
                            <li class="nav-item"><a class="nav-link" href="/history">History</a></li>
                        </ul>
                        <ul class="navbar-nav ms-auto mt-2">
                            <li class="nav-item"><a class="nav-link" href="/user">User</a></li>
                            <li class="nav-item"><a class="nav-link" href="/logout">Log Out</a></li>
                        </ul>
                    {% else %}
                        <ul class="navbar-nav ms-auto mt-2">
                            <li class="nav-item"><a class="nav-link" href="/register">Register</a></li>
                            <li class="nav-item"><a class="nav-link" href="/login">Log In</a></li>
                        </ul>
                    {% endif %}
                </div>
            </div>
        </nav>

        {% if get_flashed_messages() %}
            <header>
                <div class="alert alert-primary mb-0 text-center" role="alert">
                    {{ get_flashed_messages() | join(" ") }}
                </div>
            </header>
        {% endif %}

        <main class="container py-5 text-center">
            {% block main %}{% endblock %}
        </main>

        <footer class="mb-5 small text-center text-muted">
            Data provided by <a href="https://iexcloud.io/">IEX</a>
        </footer>

    </body>

</html>

Another important file provided is layout.html, which serves as a template that can be extended and used in other HTML files. This is made possible through the use of Jinja, a templating engine that is integrated into Flask. By using layout.html, we can easily maintain a consistent design and layout across all pages of the web application. The basic structure is defined in our web pages, which include the HTML head, navigation bar, and footer. We also include placeholders for content that will be filled in by other HTML files, such as the body of a page. Using Jinja, we can pass in variables from our Python code to populate these placeholders. For example, we can pass in the user's username to display a personalized greeting in the navigation bar. This makes it easy to create dynamic web pages that respond to user input and display customized content.

Functions to be Implemented

With the introduction to the topic complete, we can now start implementing the following functionalities:

  • register

  • quote

  • buy

  • index

  • sell

  • history

Register

@app.route("/register", methods=["GET", "POST"])
def register():
    """Register user"""

    # User reached route via POST (as by submitting a form via POST)
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        confirmation = request.form.get("confirmation")

        # Check for user error
        checkUsername = db.execute("SELECT COUNT(*) FROM users WHERE username = ?", username)
        if not username:
            return apology("missing username")
        elif not password:
            return apology("missing password")
        elif not confirmation:
            return apology("missing confirmation")
        elif checkUsername[0]["COUNT(*)"] == 1:
            return apology("username already exist")
        elif password != confirmation:
            return apology("passwords doesn't match")

        # Put new user inside the database
        db.execute("INSERT INTO users (username, hash) VALUES(?, ?)", username, generate_password_hash(password))

        # Log the user in after registering
        login = db.execute("SELECT * FROM users WHERE username = ?", request.form.get("username"))
        session["user_id"] = login[0]["id"]

        return redirect("/")
    else:
        return render_template("register.html")

The register functionality is what will handle user registration. It defines a route with the URL /register and specifies that it accepts both GET and POST requests. When a user submits a form via POST, the route checks for any errors in the user input, such as missing fields or an existing username. If there are no errors, the route generates a hash for the user's password, and inserts and stores the new user into the SQL database by executing "INSERT INTO users (username, hash) VALUES(?, ?)", username, generate_password_hash(password) where the question marks are substituted by the username and the password hash. Finally, the user is logged in and redirected to the homepage.

Quote

@app.route("/quote", methods=["GET", "POST"])
@login_required
def quote():
    """Get stock quote."""

    # User has reached route via POST
    if request.method == "POST":
        symbolFromUser = request.form.get("symbol")
        lookedUp = lookup(symbolFromUser)

        # Check if stock exist
        if lookedUp == None:
            return apology("stock symbol does not exist")
        else:
            stock = lookedUp["name"]
            price = usd(lookedUp["price"])
            symbol = lookedUp["symbol"]
            return render_template("quoted.html", name=stock, price=price, symbol=symbol)
    else:
        return render_template("quote.html")

The quote functionality is what will request for stock quote. The function is decorated with @login_required, meaning that the user needs to be logged in to access this page. The function first checks whether the user has reached the route via a POST request, which is typically done by submitting a form. If the user has submitted a form, the function gets the stock symbol entered by the user and uses the lookup() function to look up the stock's name, price, and symbol. If the stock does not exist, an apology message is returned to the user. If the stock does exist, the function renders a template that displays the stock's name, price, and symbol. If the user has not reached the route via a POST request, the function simply renders the template for getting a stock quote.

Buy

@app.route("/buy", methods=["GET", "POST"])
@login_required
def buy():
    """Buy shares of stock"""

    # User reached route via POST
    if request.method == "POST":

        # Put input of user in variables
        buySymbol = request.form.get("symbol")
        buyShares = request.form.get("shares")

        # Use the lookup() function
        buyLookedUp = lookup(buySymbol)

        # Check for user error
        if not buySymbol:
            return apology("missing symbol")
        elif buyLookedUp == None:
            return apology("invalid symbol")
        elif not buyShares:
            return apology("missing shares")
        elif not buyShares.isdigit():
            return apology("invalid shares")

        buyShares = int(buyShares)
        if buyShares <= 0:
            return apology("invalid shares")

        # Set important data to variables
        buyerId = db.execute("SELECT id FROM users WHERE id = ?", session["user_id"])
        buyStock = buyLookedUp["name"]
        buyPrice = buyLookedUp["price"]
        buyTime = datetime.now()

        # Calculate total money spent and set cash of user in a variable
        totalBuyPrice = buyShares * buyPrice
        cashOfBuyer = db.execute("SELECT cash FROM users WHERE id = ?", session["user_id"])

        # Check if user can afford the stock
        if cashOfBuyer[0]["cash"] < totalBuyPrice:
            return apology("can't afford")
        else:
            remainingCash = int(cashOfBuyer[0]["cash"]) - totalBuyPrice

            # Update database
            db.execute("INSERT INTO stocks (id, stock, symbol, shares, price, total, time) VALUES(?, ?, ?, ?, ?, ?, ?)",
                       buyerId[0]["id"], buyStock, buySymbol, buyShares, buyPrice, totalBuyPrice, buyTime)
            db.execute("UPDATE users SET cash = ? WHERE id = ?", remainingCash, buyerId[0]["id"])
            db.execute("UPDATE stocks SET symbol = UPPER(symbol)")

            flash("Bought!")

            return redirect("/")
    else:
        return render_template("buy.html")

Implementing the buy functionality was my favorite part of the problem set because it gave me more hands-on experience with handling and manipulating an SQL database. This feature required me to create a new table within the database to store information about each transaction. Although there are different ways to implement the buy functionality, I decided to approach it in the following way. The code first checked whether the user submitted the buy form via POST or GET. For a POST request, the code extracted the user input, such as the symbol and number of shares to buy, and called the lookup() function to get the current stock price from an external API. Basic input validation was performed to ensure the user entered a valid symbol and number of shares. The code then checked whether the user had enough cash to afford the transaction. If so, it updated the database by inserting a new row into the stocks table with the user's ID, stock's name, symbol, price, and the number of shares. The user's cash balance was also updated by subtracting the total purchase amount from their current cash balance. To provide feedback to the user, the flash() function from Flask was used to display a confirmation message, and the user was redirected back to the homepage. The render_template() function was used to display the buy.html template, which contained a simple form for the user to input the symbol and number of shares to buy.

Index

@app.route("/")
@login_required
def index():
    """Show portfolio of stocks"""

    # Get data manipulated by the user through buying and selling
    stockInfo = db.execute(
        "SELECT symbol, stock, SUM(shares) AS SHARES, price, SUM(total) AS TOTAL FROM stocks WHERE id = ? GROUP BY symbol",
        session["user_id"])

    # Get the cash of user
    leftCash = db.execute("SELECT cash FROM users WHERE id = ?", session["user_id"])

    # Get the total amount the user has spent
    totalBought = db.execute("SELECT SUM(total) FROM stocks WHERE id = ?", session["user_id"])

    # Sets the money and renders the html
    try:
        allMoney = float(leftCash[0]["cash"]) + float(totalBought[0]["SUM(total)"])
        return render_template("index.html", stocks=stockInfo, cash=usd(leftCash[0]["cash"]), totalMoney=usd(allMoney))
    except TypeError:
        allMoney = 10000.00
        return render_template("index.html", stocks=stockInfo, cash=usd(leftCash[0]["cash"]), totalMoney=usd(allMoney))

The index functionality displays a user's portfolio of stocks and their remaining cash balance. The function gets data from the database to populate the portfolio with information about the stocks that the user owns, such as the symbol, name, number of shares, and total value. It also retrieves the user's remaining cash balance and calculates the total amount of money the user has invested in their portfolio. The rendered HTML page displays this information to the user in a user-friendly way. The page is designed to be visually appealing and easy to navigate. It uses the usd() function to format the cash balance and total value of the user's portfolio to be displayed in a dollar format. This functionality is important for the user to be able to see an overview of their investments and make informed decisions about buying or selling stocks.

Sell

@app.route("/sell", methods=["GET", "POST"])
@login_required
def sell():
    """Sell shares of stock"""

    # User has reached route via POST
    if request.method == "POST":
        sellSymbol = request.form.get("symbol")
        sellShares = request.form.get("shares")

        sellLookedUp = lookup(sellSymbol)

        # Get number of shares user has
        shareAmount = db.execute("SELECT SUM(shares) FROM stocks WHERE id = ? AND symbol = ?", session["user_id"], sellSymbol)

        # Check for user error
        if not sellSymbol:
            return apology("missing symbol")
        elif sellLookedUp == None:
            return apology("invalid symbol")
        elif not sellShares:
            return apology("missing shares")
        elif not sellShares.isdigit():
            return apology("invalid shares")

        sellShares = int(sellShares)
        if sellShares <= 0 or sellShares > shareAmount[0]["SUM(shares)"]:
            return apology("invalid shares")

        # Set important data to variables
        sellerId = db.execute("SELECT id FROM users WHERE id = ?", session["user_id"])
        sellStock = sellLookedUp["name"]
        sellPrice = sellLookedUp["price"]
        totalSellPrice = sellShares * sellPrice
        sellShares = -abs(sellShares)
        sellTime = datetime.now()

        # Calculate the amount of money returned to user
        cashOfSeller = db.execute("SELECT cash FROM users WHERE id = ?", session["user_id"])
        remainingCash = int(cashOfSeller[0]["cash"]) + totalSellPrice
        totalSellPrice = -abs(totalSellPrice)

        # Update database
        db.execute("INSERT INTO stocks (id, stock, symbol, shares, price, total, time) VALUES(?, ?, ?, ?, ?, ?, ?)",
                   sellerId[0]["id"], sellStock, sellSymbol, sellShares, sellPrice, totalSellPrice, sellTime)
        db.execute("UPDATE users SET cash = ? WHERE id = ?", remainingCash, sellerId[0]["id"])
        db.execute("UPDATE stocks SET symbol = UPPER(symbol)")

        flash("Sold!")

        return redirect("/")
    else:
        symbols = db.execute("SELECT SUM(shares) AS SHARES, symbol FROM stocks WHERE id = ? GROUP BY symbol", session["user_id"])
        return render_template("sell.html", symbols=symbols)

The sell functionality is similar to the buy functionality. It allows the user to sell shares of a particular stock they have previously purchased. The sell route checks if the user has submitted the form via POST or GET. If it is a POST request, the lookup() function is used on the user's input to get the current stock price from an external API. Input validation is performed to ensure that the user has entered a valid symbol and number of shares. Then the number of shares the user has for that symbol is checked. If there are no errors, the route sets important data to variables and calculates the amount of money returned to the user. The database is then updated with the new transaction, and the user's cash balance is updated. Then the flash() function from Flask is used to display a confirmation message to the user and redirects them back to the homepage. If it is a GET request, the route gets all symbols the user has and displays them on the sell.html template for the user to select which stock they want to sell.

History

@app.route("/history")
@login_required
def history():
    """Show history of transactions"""

    # Put history of user in a variable
    transactions = db.execute("SELECT symbol, shares, price, time FROM stocks WHERE id = ?", session["user_id"])
    return render_template("history.html", transactions=transactions)

The history functionality allows users to view a record of their past transactions. This is a useful feature for users to keep track of their buying and selling activities. This is implemented using the Flask framework and the SQLite database. When a user accesses the history route, the server queries the database to retrieve the user's transaction history. The query returns a list of transactions that contain information about the symbol, shares, price, and time of the transaction. Once the transaction history has been retrieved from the database, the server renders the history.html template and passes the transaction data to the template as a variable. The template then uses a loop to display each transaction in a table format, with columns for the symbol, shares, price, and time.

Personal Touch

@app.route("/user", methods=["GET", "POST"])
@login_required
def user():
    """Change password of user"""

    # User has reached route via POST
    if request.method == "POST":

        # Prompt user for old and new password, and confirmation
        oldPassword = db.execute("SELECT hash FROM users WHERE id = ?", session["user_id"])
        currentPassword = request.form.get("current_password")
        newPassword = request.form.get("new_password")
        newConfirmation = request.form.get("new_confirmation")

        # Check for user error
        if not currentPassword or not newPassword or not newConfirmation:
            return apology("missing fields")
        elif not check_password_hash(oldPassword[0]["hash"], currentPassword):
            return apology("invalid current password")
        elif newPassword != newConfirmation:
            return apology("passwords do not match")

        # Generate new password hash
        newPasswordHash = generate_password_hash(newPassword)

        # Update password
        db.execute("UPDATE users SET hash = ? WHERE id = ?", newPasswordHash, session["user_id"])

        flash("Password Changed!")

        return redirect("/user")
    else:
        userName = db.execute("SELECT username FROM users WHERE id = ?", session["user_id"])
        return render_template("user.html", userName=userName[0]["username"])

For the personal touch required the problem set, I added a way users can change their passwords through the user functionality. The function checks for user errors such as missing fields, incorrect current passwords, and password confirmation mismatches. If all checks pass, it generates a new password hash and updates the user's password in the database. The render_template function is used to display the user.html template, which allows the user to enter their current password, new password, and new password confirmation. The template also displays the user's current username. In summary, the user function provides a simple and secure way for users to change their password. It enhances the user experience of the web application and increases user trust and satisfaction.

To conclude, I thoroughly enjoyed completing CS50's Introduction to Computer Science course and gained an immense amount of knowledge from it. I finished this problem set in the last week of December 2022, and I am proud of the progress I've made in my coding abilities. For my final project, I have chosen to develop a game in Godot with GDScript as the programming and it is currently a work in progress. I am excited to continue learning and expanding my programming skills in the future.

Thank you for reading and that is finance for you!

Â