Problem Set 9 - Finance 💹
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!
Subscribe to my newsletter
Read articles from Hoakin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Hoakin
Hoakin
Hello! I am an aspiring computer scientist. You can call me Joaquin and I am 17 years old.