-
Notifications
You must be signed in to change notification settings - Fork 0
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
Showing
19 changed files
with
617 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,242 @@ | ||
import os | ||
from datetime 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""" | ||
trades = db.execute("SELECT * FROM trades WHERE id = ?", session["user_id"]) | ||
user = db.execute("SELECT * FROM users WHERE id = ?", session["user_id"]) | ||
return render_template("index.html", trades=trades, cash=int(user[0]["cash"])) | ||
|
||
|
||
@app.route("/buy", methods=["GET", "POST"]) | ||
@login_required | ||
def buy(): | ||
"""Buy shares of stock""" | ||
cash = db.execute("SELECT * FROM users WHERE id = ?", session["user_id"]) | ||
cash = cash[0]["cash"] | ||
|
||
if request.method == "GET": | ||
return render_template("buy.html", cash=int(cash)) | ||
|
||
symbol = request.form.get("symbol") | ||
if not symbol: | ||
return apology("Missing Symbol!") | ||
|
||
|
||
shares = request.form.get("shares") | ||
if not shares: | ||
return apology("Missing Shares!") | ||
|
||
try: | ||
shares = int(shares) | ||
if shares < 0: | ||
return apology("Positive my friend!") | ||
except: | ||
return apology("Number my friend!") | ||
|
||
try: | ||
symbol_price = lookup(symbol)["price"] | ||
except: | ||
return apology("Valid stock my friend!") | ||
|
||
if int(shares) * symbol_price > int(cash): | ||
return apology("You don't have enough money!") | ||
|
||
trades = db.execute("SELECT * FROM trades WHERE symbol = ?", symbol.upper()) | ||
if not trades: | ||
db.execute("INSERT INTO trades (id, symbol, shares, price) VALUES (?, ?, ?, ?)", | ||
session["user_id"], symbol.upper(), int(shares), symbol_price) | ||
else: | ||
db.execute("UPDATE trades SET shares = ? WHERE symbol = ?", | ||
int(trades[0]["shares"]) + int(shares), symbol.upper()) | ||
db.execute("UPDATE users SET cash = ? WHERE id = ?", float( | ||
cash) - (float(shares) * float(symbol_price)), session["user_id"]) | ||
now = datetime.now() | ||
now = now.strftime('%Y-%m-%d %H:%M:%S') | ||
db.execute("INSERT INTO transactions (id, symbol, shares, price, transacted) VALUES (?, ?, ?, ?, ?)", | ||
session["user_id"], symbol, int(shares), symbol_price, now) | ||
|
||
return redirect("/") | ||
|
||
|
||
@app.route("/history") | ||
@login_required | ||
def history(): | ||
"""Show history of transactions""" | ||
transactions = db.execute("SELECT * FROM transactions WHERE id = ?", session["user_id"]) | ||
if request.method == "GET": | ||
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 == "GET": | ||
return render_template("quote.html") | ||
|
||
symbol = request.form.get("symbol") | ||
if not symbol: | ||
return apology("Missing Symbol") | ||
|
||
stock_quote = lookup(symbol) | ||
if not stock_quote: | ||
return apology("Missing Symbol") | ||
|
||
return render_template("quoted.html", symbol=stock_quote) | ||
|
||
|
||
@app.route("/register", methods=["GET", "POST"]) | ||
def register(): | ||
"""Register user""" | ||
# if the user enters the registeration page using get method | ||
if request.method == "GET": | ||
return render_template("register.html") | ||
|
||
# check mistakes in username [textfield] | ||
username = request.form.get("username") | ||
userExist = db.execute("SELECT * FROM users WHERE username = ?", username) | ||
if not username or len(userExist): | ||
return apology("Register with another name!") | ||
|
||
# check mistakes in password and cofirmed password | ||
password = request.form.get("password") | ||
samepass = request.form.get("confirmation") | ||
if not password or not samepass or password != samepass: | ||
return apology("Register with another password or check confirmation password!") | ||
|
||
# insertion : insert user with username, and hash | ||
db.execute("INSERT INTO users (username, hash) VALUES (?, ?)", | ||
username, generate_password_hash(password)) | ||
|
||
# get the user row from the data base | ||
rows = db.execute("SELECT * FROM users WHERE username = ?", username) | ||
|
||
# log the user in | ||
session["user_id"] = rows[0]["id"] | ||
|
||
flash("Registered!") | ||
return redirect("/") | ||
|
||
|
||
@app.route("/sell", methods=["GET", "POST"]) | ||
@login_required | ||
def sell(): | ||
"""Sell shares of stock""" | ||
symbols = db.execute("SELECT * FROM trades") | ||
if request.method == "GET": | ||
return render_template("sell.html", symbols=symbols) | ||
|
||
symbol = request.form.get("symbol") | ||
if not symbol: | ||
return apology("Missing Symbol") | ||
|
||
shares = request.form.get("shares") | ||
if not shares: | ||
return apology("Missing Shares") | ||
|
||
data = db.execute("SELECT * FROM trades WHERE symbol = ? AND id = ?", | ||
symbol, session["user_id"]) | ||
if int(data[0]["shares"]) < int(shares): | ||
return apology("Many Shares!") | ||
|
||
db.execute("UPDATE trades SET shares = ? WHERE symbol = ?", | ||
int(data[0]["shares"]) - int(shares), symbol) | ||
db.execute("DELETE FROM trades WHERE shares = 0") | ||
|
||
user = db.execute("SELECT * FROM users WHERE id = ?", session["user_id"]) | ||
db.execute("UPDATE users SET cash = ? WHERE id = ?", int( | ||
user[0]["cash"]) + int(shares) * float(data[0]["price"]), session["user_id"]) | ||
|
||
now = datetime.now() | ||
now = now.strftime('%Y-%m-%d %H:%M:%S') | ||
db.execute("INSERT INTO transactions (id, symbol, shares, price, transacted) VALUES (?, ?, ?, ?, ?)", | ||
session["user_id"], symbol, (-1.00 * int(shares)), data[0]["price"], now) | ||
|
||
flash("Sold!") | ||
return redirect("/") |
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,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}" |
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,5 @@ | ||
cs50 | ||
Flask | ||
Flask-Session | ||
pytz | ||
requests |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,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 %} |
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,18 @@ | ||
{% extends "layout.html" %} | ||
{% block title %} | ||
Buy | ||
{% endblock %} | ||
{% block main %} | ||
<div class="cash mb-5 text-primary-emphasis bg-primary-subtle w-50 m-auto p-4 rounded-3"> | ||
<span>Your Balance </span>{{ cash | usd }} | ||
</div> | ||
<form action="/buy" method="post"> | ||
<div class="w-50 m-auto mb-3"> | ||
<input type="text" class="form-control" placeholder="Symbol" name="symbol" autofocus autocomplete="off"/> | ||
</div> | ||
<div class="w-50 m-auto mb-3"> | ||
<input type="number" value="" min="1" class="form-control" placeholder="Shares" name="shares" autofocus autocomplete="off"/> | ||
</div> | ||
<button type="submit" class="btn btn-primary">Buy</button> | ||
</form> | ||
{% endblock %} |
Oops, something went wrong.