├── README.md ├── app.py ├── finance.db ├── helpers.py ├── requirements.txt ├── static └── styles.css └── templates ├── apology.html ├── buy.html ├── display.html ├── history.html ├── index.html ├── layout.html ├── login.html ├── register.html ├── sell.html └── stock.html /README.md: -------------------------------------------------------------------------------- 1 | **Description:** Website where users can create an account, buy, and sell imaginary stocks. 2 | 3 | For this project, I implemented the following functionality: 4 | 5 | 1. `register`: Allows a user to "register" on the site. The username and password are submitted via Flask and stored in a sqlite 3 database 6 | 2. `quote`: Allows a user to look up the price of a stock using the symbol 7 | 3. `buy`: Allows a user to buy the imaginary stock; Purchased stocks are saved to the database and money balance is updated 8 | 4. `index`: Displays an HTML summary table of the user's current funds and stocks 9 | 5. `sell`: Allows a user to sell stocks; Sold stocks are removed from the database and the money balance is updated 10 | 6. `history`: Displays an HTML table showing the transaction history for the user 11 | 12 | 13 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from cs50 import SQL 4 | from flask import Flask, flash, jsonify, redirect, render_template, request, session 5 | from flask_session import Session 6 | from tempfile import mkdtemp 7 | from werkzeug.exceptions import default_exceptions, HTTPException, InternalServerError 8 | from werkzeug.security import check_password_hash, generate_password_hash 9 | import datetime 10 | import math 11 | 12 | from helpers import apology, login_required, lookup, usd 13 | 14 | # Configure application 15 | app = Flask(__name__) 16 | 17 | # Ensure templates are auto-reloaded 18 | app.config["TEMPLATES_AUTO_RELOAD"] = True 19 | 20 | # Ensure responses aren't cached 21 | @app.after_request 22 | def after_request(response): 23 | response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" 24 | response.headers["Expires"] = 0 25 | response.headers["Pragma"] = "no-cache" 26 | return response 27 | 28 | # Custom filter 29 | app.jinja_env.filters["usd"] = usd 30 | 31 | # Configure session to use filesystem (instead of signed cookies) 32 | app.config["SESSION_FILE_DIR"] = mkdtemp() 33 | app.config["SESSION_PERMANENT"] = False 34 | app.config["SESSION_TYPE"] = "filesystem" 35 | Session(app) 36 | 37 | # Begin Model-View-Controller Logic 38 | # Configure CS50 Library to use SQLite database 39 | db = SQL("sqlite:///finance.db") 40 | 41 | # Make sure API key is set 42 | # API_KEY=pk_3eb0a1d6b21f468da0d712930549e542 43 | if not os.environ.get("API_KEY"): 44 | raise RuntimeError("API_KEY not set") 45 | 46 | @app.route("/") 47 | @login_required 48 | def index(): 49 | """Show portfolio of stocks""" 50 | stocks_owned = db.execute("SELECT symbol, SUM(shares) FROM transactions WHERE customer_id == :customer_id GROUP BY symbol",customer_id=session["user_id"]) 51 | 52 | print(stocks_owned) 53 | 54 | for i in range(len(stocks_owned)): 55 | if stocks_owned[i]['SUM(shares)'] == 0: 56 | del(stocks_owned[i]) 57 | 58 | for row in stocks_owned: 59 | row['name'] = lookup(row['symbol'])['name'] 60 | row['price'] = round(lookup(row['symbol'])['price'], 2) 61 | row['total'] = round(lookup(row['symbol'])['price'] * row['SUM(shares)'], 2) 62 | 63 | investment_total = 0 64 | 65 | for row in stocks_owned: 66 | investment_total += row['total'] 67 | 68 | investment_total = round(investment_total, 2) 69 | 70 | cash_total = db.execute("SELECT cash FROM users WHERE id == :id", id = session["user_id"] ) 71 | cash = round(float(cash_total[0]['cash']), 2) 72 | grand_total = round(investment_total + cash, 2) 73 | 74 | return render_template("index.html", stocks_owned=stocks_owned,investment_total=investment_total, cash=cash, grand_total=grand_total) 75 | 76 | 77 | @app.route("/buy", methods=["GET", "POST"]) 78 | @login_required 79 | def buy(): 80 | """Buy shares of stock""" 81 | if request.method == "GET": 82 | print(session["user_id"]) 83 | return render_template("buy.html") 84 | else: 85 | symbol = request.form.get("symbol") 86 | if not symbol: 87 | return apology("Please enter a stock symbol.",400) 88 | get_quote = lookup(symbol) 89 | if not get_quote: 90 | return apology("No symbol found.",400) 91 | 92 | shares = request.form.get("shares") 93 | if not shares: 94 | return apology("Please enter a number of shares.",400) 95 | try: 96 | if int(shares) < 0: 97 | return apology("Invalid number of shares.",400) 98 | except ValueError: 99 | return apology("Please enter a number of shares in +VE INTEGER.",400) 100 | 101 | 102 | current_price = get_quote["price"] 103 | amount_due = current_price * int(shares) 104 | 105 | balance = db.execute("SELECT cash FROM users WHERE id == :id", id=session["user_id"])[0] 106 | 107 | if amount_due <= float(balance["cash"]): 108 | can_buy = True 109 | else: 110 | return apology("Insufficent funds",400) 111 | 112 | if can_buy: 113 | new_cash = float(balance["cash"]) - amount_due 114 | now = datetime.datetime.now() 115 | db.execute("UPDATE users SET cash = :cash WHERE id == :id", cash=new_cash, id=session["user_id"]) 116 | db.execute("INSERT INTO transactions (customer_id, date, type, symbol, shares, PPS, Total_Amount) VALUES (:customer_id, :date, :type, :symbol, :shares, :PPS, :Total_Amount)", customer_id=session["user_id"], date=now, type="Buy", symbol=symbol, shares=shares, PPS=current_price, Total_Amount=amount_due) 117 | return redirect("/") 118 | 119 | 120 | @app.route("/history") 121 | @login_required 122 | def history(): 123 | history = db.execute("SELECT * FROM transactions WHERE customer_id == :customer_id ORDER BY date DESC", customer_id=session["user_id"]) 124 | print(history) 125 | return render_template("history.html", history=history) 126 | 127 | 128 | @app.route("/login", methods=["GET", "POST"]) 129 | def login(): 130 | """Log user in""" 131 | 132 | # Forget any user_id 133 | session.clear() 134 | 135 | # User reached route via POST (as by submitting a form via POST) 136 | if request.method == "POST": 137 | 138 | # Ensure username was submitted 139 | if not request.form.get("username"): 140 | return apology("must provide username", 400) 141 | 142 | # Ensure password was submitted 143 | elif not request.form.get("password"): 144 | return apology("must provide password", 400) 145 | 146 | # Query database for username 147 | rows = db.execute("SELECT * FROM users WHERE username = :username", 148 | username=request.form.get("username")) 149 | 150 | # Ensure username exists and password is correct 151 | if not check_password_hash(rows[0]["hash"], request.form.get("password")): 152 | return apology("invalid username and/or password", 400) 153 | 154 | # Remember which user has logged in 155 | session["user_id"] = rows[0]["id"] 156 | 157 | # Redirect user to home page 158 | return redirect("/") 159 | 160 | # User reached route via GET (as by clicking a link or via redirect) 161 | else: 162 | return render_template("login.html") 163 | 164 | 165 | @app.route("/logout") 166 | def logout(): 167 | """Log user out""" 168 | 169 | # Forget any user_id 170 | session.clear() 171 | 172 | # Redirect user to login form 173 | return redirect("/") 174 | 175 | 176 | @app.route("/quote", methods=["GET", "POST"]) 177 | @login_required 178 | def quote(): 179 | """Get stock quote.""" 180 | if request.method == "GET": 181 | return render_template("stock.html") 182 | else: 183 | symbol = request.form.get("symbol") 184 | if not symbol: 185 | return apology("Please enter a stock symbol.",400) 186 | get_quote = lookup(symbol) 187 | if not get_quote: 188 | return apology("No symbol found.",400) 189 | name = get_quote["name"] 190 | price = get_quote["price"] 191 | return render_template("display.html", name=name, symbol=symbol.upper(), price=price) 192 | 193 | 194 | @app.route("/register", methods=["GET", "POST"]) 195 | def register(): 196 | """Register user""" 197 | if request.method == "GET": 198 | return render_template("register.html") 199 | else: 200 | username = request.form.get("username") 201 | password1 = request.form.get("password") 202 | password2 = request.form.get("confirmation") 203 | 204 | special_symbols = ['!', '#', '$', '%', '.', '_', '&'] 205 | 206 | if not username: 207 | return apology("You must provide a username.",400) 208 | 209 | if not password1: 210 | 211 | return apology("You must provide a password.",400) 212 | 213 | if not password2: 214 | return apology("You must confirm your password.",400) 215 | 216 | # if len(password1) < 8: 217 | # return render_template("apology.html", message="Your password must contain 8 or more characters.") 218 | 219 | # if not any(char.isdigit() for char in password1): 220 | # return render_template("apology.html", message="Your password must contain at least 1 number.") 221 | 222 | # if not any(char.isupper() for char in password1): 223 | # return render_template("apology.html", message="Your password must contain at least uppercase letter.") 224 | 225 | # if not any(char in special_symbols for char in password1): 226 | # return render_template("apology.html", message="Your password must contain at least 1 approved symbol.") 227 | 228 | if password1 == password2: 229 | password = password1 230 | else: 231 | return apology("Password mismatch.",400) 232 | 233 | p_hash = generate_password_hash(password, method = 'pbkdf2:sha256', salt_length = 8) 234 | 235 | if len(db.execute("SELECT username FROM users WHERE username == :username", username=username)) == 0: 236 | db.execute("INSERT INTO users (username, hash) VALUES (:username, :hash)", username=username, hash=p_hash) 237 | row=db.execute("SELECT * FROM users WHERE username == :username", username=username) 238 | session["user_id"] =row[0]["id"] 239 | return redirect("/") 240 | else: 241 | return apology("Username already exists. Please enter a new username.",400) 242 | 243 | 244 | 245 | @app.route("/sell", methods=["GET", "POST"]) 246 | @login_required 247 | def sell(): 248 | """Sell shares of stock""" 249 | if request.method == "GET": 250 | return render_template("sell.html") 251 | else: 252 | symbol = request.form.get("symbol") 253 | if not symbol: 254 | return apology("Please enter a stock symbol.",400) 255 | get_quote = lookup(symbol) 256 | if not get_quote: 257 | return apology("No symbol found.",400) 258 | shares = request.form.get("shares") 259 | if not shares: 260 | return apology("Please enter a number of shares.",400) 261 | if int(shares) < 0: 262 | return apology("Invalid number of shares.",400) 263 | 264 | current_price = get_quote["price"] 265 | amount_owed = current_price * int(shares) 266 | 267 | stocks_owned = db.execute("SELECT symbol, SUM(shares) FROM transactions WHERE customer_id == :customer_id GROUP BY symbol", customer_id=session["user_id"]) 268 | stocks_dict = {} 269 | for row in stocks_owned: 270 | stocks_dict[row['symbol']] = row['SUM(shares)'] 271 | 272 | shares_available = stocks_dict[symbol] 273 | print(shares_available) 274 | 275 | if int(shares) <= int(shares_available): 276 | balance = db.execute("SELECT cash FROM users WHERE id == :id", id=session["user_id"])[0] 277 | new_balance = round(balance['cash'] + amount_owed, 2) 278 | now = datetime.datetime.now() 279 | db.execute("UPDATE users SET cash = :cash WHERE id == :id", cash=new_balance, id=session["user_id"]) 280 | db.execute("INSERT INTO transactions (customer_id, date, type, symbol, shares, PPS, Total_Amount) VALUES (:customer_id, :date, :type, :symbol, :shares, :PPS, :Total_Amount)", customer_id=session["user_id"], date=now, type="Sell", symbol=symbol, shares = -1 * int(shares), PPS=current_price, Total_Amount= -1 *amount_owed) 281 | return redirect("/") 282 | else: 283 | return apology("You are attempting to sell more shares than you own.",400) 284 | 285 | 286 | def errorhandler(e): 287 | """Handle error""" 288 | if not isinstance(e, HTTPException): 289 | e = InternalServerError() 290 | return apology(e.name, e.code) 291 | 292 | 293 | # Listen for errors 294 | for code in default_exceptions: 295 | app.errorhandler(code)(errorhandler) 296 | -------------------------------------------------------------------------------- /finance.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazyBoss07/Web-Stock-UI/8e1e2321837fd755f15f663c6790fb400e63ed06/finance.db -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import urllib.parse 4 | 5 | from flask import redirect, render_template, request, session 6 | from functools import wraps 7 | 8 | 9 | def apology(message, code=400): 10 | """Render message as an apology to user.""" 11 | def escape(s): 12 | """ 13 | Escape special characters. 14 | 15 | https://github.com/jacebrowning/memegen#special-characters 16 | """ 17 | for old, new in [("-", "--"), (" ", "-"), ("_", "__"), ("?", "~q"), 18 | ("%", "~p"), ("#", "~h"), ("/", "~s"), ("\"", "''")]: 19 | s = s.replace(old, new) 20 | return s 21 | return render_template("apology.html", top=code, bottom=escape(message)), code 22 | 23 | 24 | def login_required(f): 25 | """ 26 | Decorate routes to require login. 27 | 28 | http://flask.pocoo.org/docs/1.0/patterns/viewdecorators/ 29 | """ 30 | @wraps(f) 31 | def decorated_function(*args, **kwargs): 32 | if session.get("user_id") is None: 33 | return redirect("/login") 34 | return f(*args, **kwargs) 35 | return decorated_function 36 | 37 | 38 | def lookup(symbol): 39 | """Look up quote for symbol.""" 40 | 41 | # Contact API 42 | try: 43 | # api_key = "pk_54564a39c46e49608d59215ac1bb1b84" 44 | api_key = os.environ.get("API_KEY") 45 | # response = requests.get(f"https://cloud-sse.iexapis.com/stable/stock/{urllib.parse.quote_plus(symbol)}/quote?token={api_key}") 46 | response = requests.get(f"https://cloud.iexapis.com/stable/stock/{urllib.parse.quote_plus(symbol)}/quote?token={api_key}") 47 | response.raise_for_status() 48 | except requests.RequestException: 49 | return None 50 | 51 | # Parse response 52 | try: 53 | quote = response.json() 54 | return { 55 | "name": quote["companyName"], 56 | "price": float(quote["latestPrice"]), 57 | "symbol": quote["symbol"] 58 | } 59 | except (KeyError, TypeError, ValueError): 60 | return None 61 | 62 | 63 | def usd(value): 64 | """Format value as USD.""" 65 | return f"${value:,.2f}" 66 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cs50 2 | Flask 3 | Flask-Session 4 | requests 5 | -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | /* Size for brand */ 2 | nav .navbar-brand 3 | { 4 | font-size: xx-large; 5 | } 6 | 7 | /* Colors for brand */ 8 | nav .navbar-brand .blue 9 | { 10 | color: #537fbe; 11 | } 12 | nav .navbar-brand .red 13 | { 14 | color: #ea433b; 15 | } 16 | nav .navbar-brand .yellow 17 | { 18 | color: #f5b82e; 19 | } 20 | nav .navbar-brand .green 21 | { 22 | color: #2e944b; 23 | } 24 | -------------------------------------------------------------------------------- /templates/apology.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Apology 5 | {% endblock %} 6 | 7 | {% block main %} 8 | 9 | {{ top }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /templates/buy.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Buy Stocks 5 | {% endblock %} 6 | 7 | {% block main %} 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /templates/display.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Quote Stock 5 | {% endblock %} 6 | 7 | {% block main %} 8 | 9 | {{symbol}} 10 | {{name}} 11 | {{price}} 12 | 28.00 13 | 14 | 15 | {% endblock %} -------------------------------------------------------------------------------- /templates/history.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Account History 5 | {% endblock %} 6 | 7 | {% block main %} 8 |

Account History

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for item in history %} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 | 34 |
Transaction IdDateTransaction TypeSymbolNumber of SharesPrice Per ShareTransaction Total
{{ item["transaction_id"] }}{{ item["date"] }}{{ item["type"] }}{{ item["symbol"] }}{{ item["shares"] }} ${{ item["PPS"] }} ${{ item["Total_Amount"] }}
35 | 36 | {% endblock %} -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Your Account 5 | {% endblock %} 6 | 7 | {% block main %} 8 |

Account Summary

9 |
You have ${{ grand_total }} available.
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
Account TypeTotal
Investments${{ investment_total }}
Cash${{ cash }}
34 | 35 | 36 | 37 |
38 |

Stocks Owned

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {% for item in stocks_owned %} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% endfor %} 59 | 60 |
SymbolNameShares OwnedCurrent PriceTotal Value
{{ item["symbol"] }}{{ item["name"] }}{{ item["SUM(shares)"] }}${{ item["price"] }}${{ item["total"] }}
61 | 112.00 62 | 9,888.00 63 | 56.00 64 | 9944.00 65 | {% endblock %} -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | C$50 Finance: {% block title %}{% endblock %} 20 | 21 | 22 | 23 | 24 | 25 | 51 | 52 | {% if get_flashed_messages() %} 53 |
54 | 57 |
58 | {% endif %} 59 | 60 |
61 | {% block main %}{% endblock %} 62 |
63 | 64 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Log In 5 | {% endblock %} 6 | 7 | {% block main %} 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 | 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Register 5 | {% endblock %} 6 | 7 | {% block main %} 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 |

Passwords must be at least 8 characters long and contain at least one number, one uppercase letter, and a special symbol (!, #, $, %, . , _ , & )

22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/sell.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Sell Stocks 5 | {% endblock %} 6 | 7 | {% block main %} 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 | 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/stock.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Stock 5 | {% endblock %} 6 | 7 | {% block main %} 8 |
9 |
10 | 11 |
12 | 13 |
14 | {% endblock %} --------------------------------------------------------------------------------