bot50 2024-04-26 05:57:52 +00:00
commit 64c03bd66e
19 changed files with 664 additions and 0 deletions

277
app.py Normal file
View File

@ -0,0 +1,277 @@
import os
import datetime
from cs50 import SQL
from flask import Flask, flash, redirect, render_template, request, session
from flask_session import Session
from werkzeug.security import check_password_hash, generate_password_hash
from helpers import apology, login_required, lookup, usd
# Configure application
app = Flask(__name__)
# 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")
@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"""
user_id = session["user_id"]
cash = db.execute("SELECT * FROM users where id = ?", user_id)
stocks = db.execute(
"SELECT symbol, sum(shares) FROM transactions WHERE user_id = ? GROUP BY symbol HAVING sum(shares) > 0", user_id)
stocks_total = 0
for stock in stocks:
symbol = stock["symbol"]
# Update share price
stock_price = lookup(symbol.upper())
stock["shares"] = stock["sum(shares)"]
stock["price"] = stock_price["price"]
# Get total value of owned stock
stock["total"] = stock_price["price"] * stock["sum(shares)"]
# Get value of all owned stocks
stocks_total = stocks_total + (stock_price["price"] * stock["sum(shares)"])
# Get total value of stocks and cash
total = cash[0]["cash"] + stocks_total
return render_template("home.html", cash=cash[0]["cash"], stocks=stocks, total=total)
# return apology("MOFO")
@app.route("/buy", methods=["GET", "POST"])
@login_required
def buy():#
"""Buy shares of stock"""
if request.method == "GET":
return render_template("buy.html")
else:
symbol = request.form.get("symbol")
shares = request.form.get("shares")
if not symbol:
return apology("Not Symbol")
stock = lookup(symbol.upper())
if stock == None:
return apology("Symbol not found")
if not shares or shares.isalpha() or not float(shares).is_integer():
return apology("invalid shares")
else:
if float(shares) > 0:
transaction_value = float(shares) * stock["price"]
user_id = session["user_id"]
user_cash_db = db.execute("SELECT cash FROM users WHERE id = ?", user_id)
user_cash = user_cash_db[0]["cash"]
if user_cash < transaction_value:
return apology("U broke, m8!")
free_cash = user_cash - transaction_value
db.execute("UPDATE users SET cash = ? WHERE id = ?", free_cash, user_id)
date = datetime.datetime.now()
db.execute("INSERT INTO transactions (user_id, symbol, shares, price, date) VALUES (?, ?, ?, ?, ?)",
user_id, stock["symbol"], shares, stock["price"], date)
flash("Bought!")
else:
return apology("Missing shares", 400)
return redirect("/")
# return apology("TODO")
@app.route("/history")
@login_required
def history():
"""Show history of transactions"""
stocks = db.execute("SELECT * FROM transactions")
return render_template("history.html", stocks=stocks)
# 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."""
if request.method == "POST":
symbol = request.form.get("symbol")
if not symbol:
return apology("Not Symbol")
stock = lookup(symbol.upper())
if stock == None:
return apology("Symbol not found")
stock["price"] = usd(stock["price"])
return render_template("quote.html", stock=stock)
else:
return render_template("quote.html")
# return apology("TODO")
@app.route("/register", methods=["GET", "POST"])
def register():
"""Register user"""
if request.method == "POST":
# Ensure username was submitted
if not request.form.get("username"):
return apology("must provide username", 400)
# Ensure password was submitted
elif not request.form.get("password"):
return apology("must provide password", 400)
# Ensure password repeat matches
if not request.form.get("password") == request.form.get("confirmation"):
return apology("passwords don't match", 400)
username = request.form.get("username")
password = request.form.get("password")
username_exists = db.execute("SELECT * FROM users WHERE username = ?", username)
if username_exists:
return apology("username already exists!", 400)
db.execute("INSERT INTO users(username, hash) VALUES(?, ?)",
username, generate_password_hash(password))
return redirect("/")
else:
return render_template("register.html")
# return apology("MOFO")
@app.route("/sell", methods=["GET", "POST"])
@login_required
def sell():
"""Sell shares of stock"""
user_id = session["user_id"]
if request.method == "POST":
symbol = request.form.get("symbol")
shares = int(request.form.get("shares"))
if not symbol:
return apology("Not Symbol")
stock = lookup(symbol.upper())
if stock == None:
return apology("Symbol not found")
stocks = db.execute(
"SELECT symbol, sum(shares) FROM transactions WHERE user_id = ? AND symbol = ?", user_id, symbol)
if stocks[0]["sum(shares)"] < shares:
return apology("Not enough shares")
transaction_value = shares * stock["price"]
user_cash_db = db.execute("SELECT cash FROM users WHERE id = ?", user_id)
user_cash = user_cash_db[0]["cash"]
new_cash = transaction_value + user_cash
db.execute("UPDATE users SET cash = ? WHERE id = ?", new_cash, user_id)
date = datetime.datetime.now()
db.execute("INSERT INTO transactions (user_id, symbol, shares, price, date) VALUES (?, ?, ?, ?, ?)",
user_id, stock["symbol"], -abs(shares), stock["price"], date)
flash("Sold!")
return redirect("/")
else:
stocks = db.execute(
"SELECT symbol FROM transactions WHERE user_id = ? GROUP BY symbol HAVING sum(shares) > 0", user_id)
return render_template("sell.html", stocks=stocks)
# return apology("TODO")

BIN
finance.db Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

88
helpers.py Normal file
View File

@ -0,0 +1,88 @@
import csv
import datetime
import pytz
import requests
import urllib
import uuid
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/latest/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."""
# Prepare API request
symbol = symbol.upper()
end = datetime.datetime.now(pytz.timezone("US/Eastern"))
start = end - datetime.timedelta(days=7)
# Yahoo Finance API
url = (
f"https://query1.finance.yahoo.com/v7/finance/download/{urllib.parse.quote_plus(symbol)}"
f"?period1={int(start.timestamp())}"
f"&period2={int(end.timestamp())}"
f"&interval=1d&events=history&includeAdjustedClose=true"
)
# Query API
try:
response = requests.get(
url,
cookies={"session": str(uuid.uuid4())},
headers={"Accept": "*/*", "User-Agent": request.headers.get("User-Agent")},
)
response.raise_for_status()
# CSV header: Date,Open,High,Low,Close,Adj Close,Volume
quotes = list(csv.DictReader(response.content.decode("utf-8").splitlines()))
price = round(float(quotes[-1]["Adj Close"]), 2)
return {"price": price, "symbol": symbol}
except (KeyError, IndexError, requests.RequestException, ValueError):
return None
def usd(value):
"""Format value as USD."""
return f"${value:,.2f}"

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
cs50
Flask
Flask-Session
pytz
requests

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

23
static/styles.css Normal file
View File

@ -0,0 +1,23 @@
/* Size for brand */
nav .navbar-brand
{
font-size: xx-large;
}
/* Colors for brand */
nav .navbar-brand .blue
{
color: #537fbe;
}
nav .navbar-brand .red
{
color: #ea433b;
}
nav .navbar-brand .yellow
{
color: #f5b82e;
}
nav .navbar-brand .green
{
color: #2e944b;
}

11
templates/apology.html Normal file
View File

@ -0,0 +1,11 @@
{% extends "layout.html" %}
{% block title %}
Apology
{% endblock %}
{% block main %}
<!-- https://memegen.link/ -->
<!-- https://knowyourmeme.com/memes/grumpy-cat -->
<img alt="{{ top }}" class="border img-fluid" src="https://api.memegen.link/images/custom/{{ top | urlencode }}/{{ bottom | urlencode }}.jpg?background=https://i.imgur.com/CsCgN7Ll.png&width=400" title="{{ top }}">
{% endblock %}

17
templates/buy.html Normal file
View File

@ -0,0 +1,17 @@
{% extends "layout.html" %}
{% block title %}
Buy
{% endblock %}
{% block main %}
<form action="/buy" method="POST">
<div class="mb-3">
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus="" name="symbol" placeholder="Symbol" type="text">
</div>
<div class="mb-3">
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus min="1" name="shares" placeholder="Shares" type="text">
</div>
<button class="btn btn-primary" type="submit">Buy</button>
</form>
{% endblock %}

30
templates/history.html Normal file
View File

@ -0,0 +1,30 @@
{% extends "layout.html" %}
{% block title %}
History
{% endblock %}
{% block main %}
<table class="table">
<thead>
<tr>
<th class="text-start">Symbol</th>
<th class="text-end">Shares</th>
<th class="text-end">Price</th>
<th class="text-end">Transacted</th>
</tr>
</thead>
<tbody>
{% for stock in stocks%}
<tr>
<td class="text-start">{{ stock.symbol }}</td>
<td class="text-end">{{ stock.shares }}</td>
<td class="text-end">{{ stock.price | usd}}</td>
<td class="text-end">{{ stock.date }}</td>
</tr>
{% endfor%}
</tbody>
</table>
{% endblock %}

40
templates/home.html Normal file
View File

@ -0,0 +1,40 @@
{% extends "layout.html" %}
{% block title %}
Home
{% endblock %}
{% block main %}
<table class="table table-striped">
<thead>
<tr>
<th class="text-start">Symbol</th>
<th class="text-end">Shares</th>
<th class="text-end">Price</th>
<th class="text-end">TOTAL</th>
</tr>
</thead>
<tbody>
{% for stock in stocks%}
<tr>
<td class="text-start">{{ stock.symbol }}</td>
<td class="text-end">{{ stock.shares }}</td>
<td class="text-end">{{ stock.price | usd }}</td>
<td class="text-end">{{ stock.total | usd }}</td>
</tr>
{% endfor%}
</tbody>
<tfoot>
<tr>
<td class="border-0 fw-bold text-end" colspan="3">Cash</td>
<td class="border-0 text-end" colspan="3">{{ cash | usd }}</td>
</tr>
<tr>
<td class="border-0 fw-bold text-end" colspan="3">TOTAL</td>
<td class="border-0 fw- bold text-end" colspan="3">{{ total | usd }}</td>
</tr>
</tfoot>
</table>
{% endblock %}

91
templates/layout.html Normal file
View File

@ -0,0 +1,91 @@
<!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.3/ -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></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="/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">
<p class="mb-3 small text-center text-muted">
Data provided by <a href="https://finance.yahoo.com/">Yahoo</a>
</p>
<form action="https://validator.w3.org/check" class="text-center" enctype="multipart/form-data" method="post" target="_blank">
<input name="doctype" type="hidden" value="HTML5">
<input name="fragment" type="hidden">
<input alt="Validate" src="/static/I_heart_validator.png" type="image"> <!-- https://validator.w3.org/ -->
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Adapted from https://stackoverflow.com/a/10162353
const html = '<!DOCTYPE ' +
document.doctype.name +
(document.doctype.publicId ? ' PUBLIC "' + document.doctype.publicId + '"' : '') +
(!document.doctype.publicId && document.doctype.systemId ? ' SYSTEM' : '') +
(document.doctype.systemId ? ' "' + document.doctype.systemId + '"' : '') +
'>\n' + document.documentElement.outerHTML;
document.querySelector('form[action="https://validator.w3.org/check"] > input[name="fragment"]').value = html;
});
</script>
</footer>
</body>
</html>

17
templates/login.html Normal file
View File

@ -0,0 +1,17 @@
{% extends "layout.html" %}
{% block title %}
Log In
{% endblock %}
{% block main %}
<form action="/login" method="post">
<div class="mb-3">
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus name="username" placeholder="Username" type="text">
</div>
<div class="mb-3">
<input class="form-control mx-auto w-auto" name="password" placeholder="Password" type="password">
</div>
<button class="btn btn-primary" type="submit">Log In</button>
</form>
{% endblock %}

23
templates/quote.html Normal file
View File

@ -0,0 +1,23 @@
{% extends "layout.html" %}
{% block title %}
Quote
{% endblock %}
{% block main %}
{% if stock %}
<p>A share of {{ stock.symbol }} costs {{ stock.price }}</p>
{% else%}
<form action="/quote" method="POST">
<div class="mb-3">
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus="" name="symbol" placeholder="Symbol" type="text">
</div>
<button class="btn btn-primary" type="submit">Quote</button>
</form>
{% endif %}
{% endblock %}

20
templates/register.html Normal file
View File

@ -0,0 +1,20 @@
{% extends "layout.html" %}
{% block title %}
Register
{% endblock %}
{% block main %}
<form action="/register" method="post">
<div class="mb-3">
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus name="username" placeholder="Username" type="text">
</div>
<div class="mb-3">
<input class="form-control mx-auto w-auto" name="password" placeholder="Password" type="password">
</div>
<div class="mb-3">
<input class="form-control mx-auto w-auto" name="confirmation" placeholder="Password (again)" type="password">
</div>
<button class="btn btn-primary" type="submit">Register</button>
</form>
{% endblock %}

22
templates/sell.html Normal file
View File

@ -0,0 +1,22 @@
{% extends "layout.html" %}
{% block title %}
Sell
{% endblock %}
{% block main %}
<form action="/sell" method="POST">
<div class="mb-3">
<select class="form-select mx-auto w-auto" name="symbol">
<option disables="" selected="">Symbol</option>
{% for stock in stocks %}
<option value="{{ stock.symbol }}">{{ stock.symbol }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus min="1" name="shares" placeholder="Shares" type="number">
</div>
<button class="btn btn-primary" type="submit">Sell</button>
</form>
{% endblock %}