-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 735ed44
Showing
21 changed files
with
579 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
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""" | ||
userID = session["user_id"] | ||
symbols = db.execute("SELECT DISTINCT symbol FROM transactions WHERE user_id == ?", userID) | ||
assets = [] | ||
rows = db.execute("SELECT cash from users WHERE id = ?", userID ) | ||
current_cash = rows[0]["cash"] | ||
total = current_cash | ||
for symbol in symbols: | ||
|
||
shares_bought = db.execute("SELECT SUM(shares) FROM transactions WHERE user_id == :userID AND symbol == :symbol AND type == 'buy'", userID = userID, symbol = symbol['symbol']) | ||
shares_sold = db.execute("SELECT SUM(shares) FROM transactions WHERE user_id == :userID AND symbol == :symbol AND type == 'sell'", userID = userID, symbol = symbol['symbol']) | ||
if shares_sold[0]['SUM(shares)'] is None: | ||
shares_sold[0]['SUM(shares)'] = 0 | ||
net_shares = shares_bought[0]['SUM(shares)'] - shares_sold[0]['SUM(shares)'] | ||
|
||
if not net_shares == 0: | ||
assets.append({"symbol": symbol["symbol"], "name" : lookup(symbol["symbol"])['name'] , "shares" : net_shares , "price" : lookup(symbol["symbol"])['price'] }) | ||
for asset in assets: | ||
total = total + (asset['price'] * asset['shares']) | ||
|
||
return render_template("index.html" , current_cash = current_cash, assets = assets , total = total) | ||
|
||
|
||
@app.route("/buy", methods=["GET", "POST"]) | ||
@login_required | ||
def buy(): | ||
"""Buy shares of stock""" | ||
userID = session["user_id"] | ||
if request.method == "POST": | ||
symbol = request.form.get("symbol") | ||
if not lookup(symbol): | ||
return apology("invalid symbol") | ||
if not request.form.get("shares") or not request.form.get("shares").isnumeric(): | ||
return apology("please specify shares", 400) | ||
else: | ||
price = lookup(symbol)["price"] | ||
stock = lookup(symbol)["name"] | ||
|
||
shares = int(request.form.get("shares")) | ||
rows = db.execute("SELECT cash from users WHERE id = ?", userID ) | ||
current_cash = rows[0]["cash"] | ||
if current_cash - (shares * price) < 0: | ||
return apology("not enough funds") | ||
else: | ||
# make transaction | ||
current_cash = current_cash - (shares * price) | ||
db.execute("UPDATE users SET cash= :current_cash WHERE id= :userID" , current_cash = current_cash, userID= userID) | ||
db.execute("CREATE TABLE IF NOT EXISTS transactions (transaction_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id TEXT NOT NULL, stock TEXT NOT NULL, symbol TEXT NOT NULL, price NUMERIC NOT NULL, shares INTEGER NOT NULL, type TEXT NOT NULL, time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id));") | ||
db.execute("INSERT INTO transactions (user_id, stock, symbol, price, shares, type) VALUES(?, ?, ?, ?, ?, ?)", userID, stock, symbol, usd(price), shares, 'buy') | ||
return redirect("/") | ||
|
||
else: | ||
return render_template("buy.html") | ||
|
||
|
||
|
||
@app.route("/history") | ||
@login_required | ||
def history(): | ||
"""Show history of transactions""" | ||
userID = session["user_id"] | ||
transactions = db.execute("SELECT symbol, shares, type, price, time FROM transactions WHERE user_id == ?", userID) | ||
|
||
|
||
return render_template("history.html", transactions = transactions) | ||
|
||
|
||
@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 lookup(symbol): | ||
return apology("invalid symbol") | ||
else: | ||
return render_template("quoted.html", quote = lookup(symbol)) | ||
else: | ||
return render_template("quote.html") | ||
|
||
|
||
@app.route("/register", methods=["GET", "POST"]) | ||
def register(): | ||
"""Register user""" | ||
# Forget any user_id | ||
session.clear() | ||
|
||
# User reached route via POST (as by submitting a form via POST) | ||
if request.method == "POST": | ||
name = request.form.get("username") | ||
rows = db.execute("SELECT * FROM users WHERE username = ?", request.form.get("username")) | ||
if not name or len(rows) > 0: | ||
return apology("Invalid username", 400) | ||
# Ensure password was submitted | ||
|
||
elif not request.form.get("password"): | ||
return apology("must provide password", 400) | ||
elif not request.form.get("password") == request.form.get("confirmation"): | ||
return apology("password doesn't match", 400) | ||
|
||
hash = generate_password_hash(request.form.get("password")) | ||
db.execute("INSERT INTO users (username, hash) VALUES(?, ?)", name , hash ) | ||
|
||
return redirect("/") | ||
|
||
# User reached route via GET (as by clicking a link or via redirect) | ||
else: | ||
return render_template("register.html") | ||
|
||
|
||
|
||
|
||
|
||
@app.route("/sell", methods=["GET", "POST"]) | ||
@login_required | ||
def sell(): | ||
|
||
"""Sell shares of stock""" | ||
userID = session["user_id"] | ||
|
||
|
||
if request.method == "POST": | ||
symbol = request.form.get("symbol") | ||
shares_bought = db.execute("SELECT SUM(shares) FROM transactions WHERE user_id == :userID AND symbol == :symbol AND type == 'buy'", userID = userID, symbol = symbol) | ||
shares_sold = db.execute("SELECT SUM(shares) FROM transactions WHERE user_id == :userID AND symbol == :symbol AND type == 'sell'", userID = userID, symbol = symbol) | ||
# rows = db.execute("SELECT SUM(shares) - (SELECT SUM(shares) FROM transactions WHERE user_id == :userID AND symbol == :symbol AND type == 'sell') AS net_shares FROM transactions WHERE user_id == :userID AND symbol == :symbol AND type == 'buy'", userID = userID, symbol = symbol) | ||
if shares_sold[0]['SUM(shares)'] is None: | ||
shares_sold[0]['SUM(shares)'] = 0 | ||
net_shares = shares_bought[0]['SUM(shares)'] - shares_sold[0]['SUM(shares)'] | ||
shares_to_sell = int(request.form.get("shares")) | ||
if not symbol: | ||
return apology("INVALID SYMBOL") | ||
elif shares_to_sell < 0 or (net_shares - shares_to_sell) < 0: | ||
return apology("invalid number of shares") | ||
else: | ||
price = lookup(symbol)["price"] | ||
stock = lookup(symbol)["name"] | ||
difference = int(shares_to_sell) * price | ||
cash_list = db.execute("SELECT cash from users WHERE id = ?", userID ) | ||
current_cash = cash_list[0]["cash"] | ||
current_cash = current_cash + difference | ||
db.execute("UPDATE users SET cash= :current_cash WHERE id= :userID" , current_cash = current_cash, userID= userID) | ||
db.execute("INSERT INTO transactions (user_id, stock, symbol, price, shares, type) VALUES(?, ?, ?, ?, ?, ?)", userID, stock, symbol, usd(price), shares_to_sell, 'sell') | ||
return redirect("/") | ||
|
||
else: | ||
|
||
user_stocks = db.execute("SELECT DISTINCT symbol FROM transactions WHERE user_id == ?", userID ) | ||
return render_template("sell.html", options = user_stocks) |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
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}" |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pk_5824394ae4834a1abfb912ed8ba1cbbb |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
cs50 | ||
Flask | ||
Flask-Session | ||
gunicorn | ||
psycopg2 | ||
requests |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{% extends "layout.html" %} | ||
|
||
{% block title %} | ||
Apology | ||
{% endblock %} | ||
|
||
{% block main %} | ||
<!-- https://memegen.link/ --> | ||
<img alt="{{ top }}" class="border img-fluid" src="http://memegen.link/custom/{{ top | urlencode }}/{{ bottom | urlencode }}.jpg?alt=https://i.imgur.com/WkyZxDD.jpg&width=400" title="{{ top }}"> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{% extends "layout.html" %} | ||
|
||
{% block title %} | ||
Buy | ||
{% endblock %} | ||
|
||
{% block main %} | ||
<form action="/buy" method="post"> | ||
<input class="form-control mx-auto w-auto mb-3" id="symbol" type="text" name="symbol" placeholder="Symbol"> | ||
<input class="form-control mx-auto w-auto mb-3" id="shares" type="number" name="shares" placeholder="shares" min="1" > | ||
<button class="btn btn-primary" type="submit">Buy</button> | ||
</form> | ||
|
||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{% extends "layout.html" %} | ||
|
||
{% block title %} | ||
History | ||
{% endblock %} | ||
|
||
{% block main %} | ||
<table class="table table-striped" > | ||
<thead> | ||
<tr > | ||
<td class="fw-bold">Symbol</td> | ||
|
||
<td class="fw-bold">Shares</td> | ||
<td class="fw-bold">Type</td> | ||
<td class="fw-bold">Price</td> | ||
<td class="fw-bold">Time</td> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{% for transaction in transactions %} | ||
<tr> | ||
<td>{{ transaction.symbol | upper() }}</td> | ||
<td>{{ transaction.shares }}</td> | ||
<td>{{ transaction.type }}</td> | ||
<td>{{ transaction.price }}</td> | ||
<td>{{ transaction.time }}</td> | ||
</tr> | ||
{% endfor %} | ||
{% endblock %} |
Oops, something went wrong.