├── 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 |
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/templates/buy.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 | {% block title %}
4 | Buy Stocks
5 | {% endblock %}
6 |
7 | {% block main %}
8 |
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 | Transaction Id |
13 | Date |
14 | Transaction Type |
15 | Symbol |
16 | Number of Shares |
17 | Price Per Share |
18 | Transaction Total |
19 |
20 |
21 |
22 | {% for item in history %}
23 |
24 | {{ item["transaction_id"] }} |
25 | {{ item["date"] }} |
26 | {{ item["type"] }} |
27 | {{ item["symbol"] }} |
28 | {{ item["shares"] }} |
29 | ${{ item["PPS"] }} |
30 | ${{ item["Total_Amount"] }} |
31 |
32 | {% endfor %}
33 |
34 |
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 | Account Type |
14 | |
15 | |
16 | Total |
17 |
18 |
19 |
20 |
21 | Investments |
22 | |
23 | |
24 | ${{ investment_total }} |
25 |
26 |
27 | Cash |
28 | |
29 | |
30 | ${{ cash }} |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Stocks Owned
39 |
40 |
41 |
42 | Symbol |
43 | Name |
44 | Shares Owned |
45 | Current Price |
46 | Total Value |
47 |
48 |
49 |
50 | {% for item in stocks_owned %}
51 |
52 | {{ item["symbol"] }} |
53 | {{ item["name"] }} |
54 | {{ item["SUM(shares)"] }} |
55 | ${{ item["price"] }} |
56 | ${{ item["total"] }} |
57 |
58 | {% endfor %}
59 |
60 |
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 |
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 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/templates/register.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 | {% block title %}
4 | Register
5 | {% endblock %}
6 |
7 | {% block main %}
8 |
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 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/templates/stock.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 | {% block title %}
4 | Stock
5 | {% endblock %}
6 |
7 | {% block main %}
8 |
14 | {% endblock %}
--------------------------------------------------------------------------------