Problem: C$50 Finance
tl;dr
Implement a website via which users can "buy" and "sell" stocks, a la the below.
Academic Honesty
This course’s philosophy on academic honesty is best stated as "be reasonable." The course recognizes that interactions with classmates and others can facilitate mastery of the course’s material. However, there remains a line between enlisting the help of another and submitting the work of another. This policy characterizes both sides of that line.
The essence of all work that you submit to this course must be your own. Collaboration on problems is not permitted (unless explicitly stated otherwise) except to the extent that you may ask classmates and others for help so long as that help does not reduce to another doing your work for you. Generally speaking, when asking for help, you may show your code or writing to others, but you may not view theirs, so long as you and they respect this policy’s other constraints. Collaboration on quizzes and tests is not permitted at all. Collaboration on the final project is permitted to the extent prescribed by its specification.
Below are rules of thumb that (inexhaustively) characterize acts that the course considers reasonable and not reasonable. If in doubt as to whether some act is reasonable, do not commit it until you solicit and receive approval in writing from your instructor. If a violation of this policy is suspected and confirmed, your instructor reserves the right to impose local sanctions on top of any disciplinary outcome that may include an unsatisfactory or failing grade for work submitted or for the course itself.
Reasonable
-
Communicating with classmates about problems in English (or some other spoken language).
-
Discussing the course’s material with others in order to understand it better.
-
Helping a classmate identify a bug in his or her code, such as by viewing, compiling, or running his or her code, even on your own computer.
-
Incorporating snippets of code that you find online or elsewhere into your own code, provided that those snippets are not themselves solutions to assigned problems and that you cite the snippets' origins.
-
Reviewing past years' quizzes, tests, and solutions thereto.
-
Sending or showing code that you’ve written to someone, possibly a classmate, so that he or she might help you identify and fix a bug.
-
Sharing snippets of your own solutions to problems online so that others might help you identify and fix a bug or other issue.
-
Turning to the web or elsewhere for instruction beyond the course’s own, for references, and for solutions to technical difficulties, but not for outright solutions to problems or your own final project.
-
Whiteboarding solutions to problems with others using diagrams or pseudocode but not actual code.
-
Working with (and even paying) a tutor to help you with the course, provided the tutor does not do your work for you.
Not Reasonable
-
Accessing a solution to some problem prior to (re-)submitting your own.
-
Asking a classmate to see his or her solution to a problem before (re-)submitting your own.
-
Decompiling, deobfuscating, or disassembling the staff’s solutions to problems.
-
Failing to cite (as with comments) the origins of code, writing, or techniques that you discover outside of the course’s own lessons and integrate into your own work, even while respecting this policy’s other constraints.
-
Giving or showing to a classmate a solution to a problem when it is he or she, and not you, who is struggling to solve it.
-
Looking at another individual’s work during a quiz or test.
-
Paying or offering to pay an individual for work that you may submit as (part of) your own.
-
Providing or making available solutions to problems to individuals who might take this course in the future.
-
Searching for, soliciting, or viewing a quiz’s questions or answers prior to taking the quiz.
-
Searching for or soliciting outright solutions to problems online or elsewhere.
-
Splitting a problem’s workload with another individual and combining your work (unless explicitly authorized by the problem itself).
-
Submitting (after possibly modifying) the work of another individual beyond allowed snippets.
-
Submitting the same or similar work to this course that you have submitted or will submit to another.
-
Using resources during a quiz beyond those explicitly allowed in the quiz’s instructions.
-
Viewing another’s solution to a problem and basing your own solution on it.
Background
If you’re not quite sure what it means to buy and sell stocks (i.e., shares of a company), head to http://www.investopedia.com/university/stocks/ for a tutorial.
You’re about to implement C$50 Finance, a web app via which you can manage portfolios of stocks. Not only will this tool allow you to check real stocks' actual prices and portfolios' values, it will also let you buy (okay, "buy") and sell (okay, "sell") stocks by querying IEX for stocks' prices.
Indeed, IEX lets you download stock quotes via their API (application programming interface) using URLs like https://cloud-sse.iexapis.com/stable/stock/NFLX/quote?token={api_key}, in which you would substitute {api_key} with an actual API key! Notice how Netflix’s symbol (NFLX) is embedded in this URL; that’s how IEX knows whose data to return. With an API key, once you follow that type of link, you’ll see a reponse in JSON (JavaScript Object Notation) format. It’s a bit messy, but one can notice how, between the curly braces, there’s a comma-separated list of key-value pairs, with a colon separating each key from its value.
Distribution
Downloading
Here’s how to download this problem’s "distribution code" (i.e., starter code) into your own CS50 IDE. Log into CS50 IDE and then, in a terminal window, execute each of the below.
-
Execute
cd
to ensure that you’re in~/
(i.e., your home directory). -
Execute
mkdir chapter8
to make (i.e., create) a directory calledchapter8
in your home directory, if you haven’t already done so. -
Execute
cd chapter8
to change into (i.e., open) that directory. -
Execute
wget http://cdn.cs50.net/2019/fall/tracks/web/finance/finance.zip
to download a (compressed) ZIP file with this problem’s distribution. -
Execute
unzip finance.zip
to uncompress that file. -
Execute
rm finance.zip
followed byyes
ory
to delete that ZIP file. -
Execute
ls
. You should see a directory calledfinance
, which was inside of that ZIP file. -
Execute
cd finance
to change into that directory. -
Execute
ls
. You should see this problem’s distribution code, includingapplication.py
,helpers.py
,finance.db
,requirements.txt
,static/
andtemplates/
.
Running
-
Start Flask’s built-in web server (within
finance/
):flask run
Visit the URL outputted by
flask
to see the distribution code in action. You won’t be able to log in or register, though, just yet! -
Via CS50’s file browser, double-click finance.db in order to open it with phpLiteAdmin. Notice how
finance.db
comes with a table calledusers
. Take a look at its structure (i.e., schema). Notice how, by default, new users will receive $10,000 in cash. But there aren’t (yet!) any users (i.e., rows) therein to browse.Here on out, if you’d prefer a command line, you’re welcome to use
sqlite3
instead of phpLiteAdmin.
Understanding
application.py
Open up application.py
. Atop the file are a bunch of imports, among them CS50’s SQL module and a few helper functions. More on those soon.
After configuring Flask, notice how this file disables caching of responses (provided you’re in debugging mode, which you are by default on CS50 IDE), lest you make a change to some file but your browser not notice. Notice next how it configures Jinja with a custom "filter," usd
, a function (defined in helpers.py
) that will make it easier to format values as US dollars (USD). It then further configures Flask to store sessions on the local filesystem (i.e., disk) as opposed to storing them inside of (digitally signed) cookies, which is Flask’s default. The file then configures CS50’s SQL module to use finance.db
, a SQLite database whose contents we’ll soon see!
Thereafter are a whole bunch of routes, only two of which are fully implemented: login
and logout
. Read through the implementation of login
first. Notice how it uses db.execute
(from CS50’s library) to query finance.db
. And notice how it uses check_password_hash
to compare hashes of users' passwords. Finally, notice how login
"remembers" that a user is logged in by storing his or her user_id
, an INTEGER, in session
. That way, any of this file’s routes can check which user, if any, is logged in. Meanwhile, notice how logout
simply clears session
, effectively logging a user out.
Notice how most routes are "decorated" with @login_required
(a function defined in helpers.py
too). That decorator ensures that, if a user tries to visit any of those routes, he or she will first be redirected to login
so as to log in.
Notice too how most routes support GET and POST. Even so, most of them (for now!) simply return an "apology," since they’re not yet implemented.
helpers.py
Next take a look at helpers.py
. Ah, there’s the implementation of apology
. Notice how it ultimately renders a template, apology.html
. It also happens to define within itself another function, escape
, that it simply uses to replace special characters in apologies. By defining escape
inside of apology
, we’ve scoped the former to the latter alone; no other functions will be able (or need) to call it.
Next in the file is login_required
. No worries if this one’s a bit cryptic, but if you’ve ever wondered how a function can return another function, here’s an example!
Thereafter is lookup
, a function that, given a symbol
(e.g., NFLX), returns a stock quote for a company in the form of a dict
with three keys: name
, whose value is a str
, the name of the company; price
, whose value is a float
; and symbol
, whose value is a str
, a canonicalized (uppercase) version of a stock’s symbol, irrespective of how that symbol was capitalized when passed into lookup
.
Last in the file is usd
, a short function that simply formats a float
as USD (e.g., 1234.56
is formatted as $1,234.56
).
requirements.txt
Next take a quick look at requirements.txt
. That file simply prescribes the packages on which this app will depend.
static/
Glance too at static/
, inside of which is styles.css
. That’s where some initial CSS lives. You’re welcome to alter it as you see fit.
templates/
Now look in templates/
. In login.html
is, essentially, just an HTML form, stylized with Bootstrap. In apology.html
, meanwhile, is a template for an apology. Recall that apology
in helpers.py
took two arguments: message
, which was passed to render_template
as the value of bottom
, and, optionally, code
, which was passed to render_template
as the value of top
. Notice in apology.html
how those values are ultimately used! And here’s why. 0:-)
Last up is layout.html
. It’s a bit bigger than usual, but that’s mostly because it comes with a fancy, mobile-friendly "navbar" (navigation bar), also based on Bootstrap. Notice how it defines a block, main
, inside of which templates (including apology.html
and login.html
) shall go. It also includes support for Flask’s message flashing so that you can relay messages from one route to another for the user to see.
Specification
register
Complete the implementation of register
in such a way that it allows a user to register for an account via a form.
-
Require that a user input a username, implemented as a text field whose
name
isusername
. Render an apology if the user’s input is blank or the username already exists. -
Require that a user input a password, implemented as a text field whose
name
ispassword
, and then that same password again, implemented as a text field whosename
isconfirmation
. Render an apology if either input is blank or the passwords do not match. -
Submit the user’s input via
POST
to/register
. -
INSERT
the new user intousers
, storing a hash of the user’s password, not the password itself. Hash the user’s password withgenerate_password_hash
. -
Odds are you’ll want to create a new template (e.g.,
register.html
) that’s quite similar tologin.html
.
Once you’ve implemented register
correctly, you should be able to register for an account and log in (since login
and logout
already work)! And you should be able to see your rows via phpLiteAdmin or sqlite3
.
quote
Complete the implementation of quote
in such a way that it allows a user to look up a stock’s current price.
-
Require that a user input a stock’s symbol, implemented as a text field whose
name
issymbol
. -
Submit the user’s input via
POST
to/quote
. -
Odds are you’ll want to create two new templates (e.g.,
quote.html
andquoted.html
). When a user visits/quote
via GET, render one of those templates, inside of which should be an HTML form that submits to/quote
via POST. In response to a POST,quote
can render that second template, embedding within it one or more values fromlookup
.
buy
Complete the implementation of buy
in such a way that it enables a user to buy stocks.
-
Require that a user input a stock’s symbol, implemented as a text field whose
name
issymbol
. Render an apology if the input is blank or the symbol does not exist (as per the return value oflookup
). -
Require that a user input a number of shares, implemented as a text field whose
name
isshares
. Render an apology if the input is not a positive integer. -
Submit the user’s input via
POST
to/buy
. -
Odds are you’ll want to call
lookup
to look up a stock’s current price. -
Odds are you’ll want to
SELECT
how much cash the user currently has inusers
. -
Add one or more new tables to
finance.db
via which to keep track of the purchase. Store enough information so that you know who bought what at what price and when.-
Use appropriate SQLite types.
-
Define
UNIQUE
indexes on any fields that should be unique. -
Define (non-
UNIQUE
) indexes on any fields via which you will search (as viaSELECT
withWHERE
).
-
-
Render an apology, without completing a purchase, if the user cannot afford the number of shares at the current price.
-
You don’t need to worry about race conditions (or use transactions).
Once you’ve implemented buy
correctly, you should be able to see users' purchases in your new table(s) via phpLiteAdmin or sqlite3
.
index
Complete the implementation of index
in such a way that it displays an HTML table summarizing, for the user currently logged in, which stocks the user owns, the numbers of shares owned, the current price of each stock, and the total value of each holding (i.e., shares times price). Also display the user’s current cash balance along with a grand total (i.e., stocks' total value plus cash).
sell
Complete the implementation of sell
in such a way that it enables a user to sell shares of a stock (that he or she owns).
-
Require that a user input a stock’s symbol, implemented as a
select
menu whosename
issymbol
. Render an apology if the user fails to select a stock or if (somehow, once submitted) the user does not own any shares of that stock. -
Require that a user input a number of shares, implemented as a text field whose
name
isshares
. Render an apology if the input is not a positive integer or if the user does not own that many shares of the stock. -
Submit the user’s input via
POST
to/sell
. -
You don’t need to worry about race conditions (or use transactions).
history
Complete the implementation of history
in such a way that it displays an HTML table summarizing all of a user’s transactions ever, listing row by row each and every buy and every sell.
-
For each row, make clear whether a stock was bought or sold and include the stock’s symbol, the (purchase or sale) price, the number of shares bought or sold, and the date and time at which the transaction occurred.
-
You might need to alter the table you created for
buy
or supplement it with an additional table. Try to minimize redundancies.
check
Complete the implementation of check
in such a way that it checks whether a username is available.
-
The route should accept, via GET, an HTTP parameter called
username
. -
If the value of
username
is of length at least 1 and does not already belong to a user in the database, the route should return, in JSON format,true
, signifying that the username is (as of that moment) available. Else it should return, in JSON format,false
. Recall thatjsonify
in Flask can return a value in JSON format.
Finally, enhance your template for register
with some JavaScript in such a way that it prevents submission of your registration form if the inputted username is already taken, letting the user know, as via alert
or via a Bootstrap alert or validation.
-
Use Ajax (e.g., jQuery’s
$.get
method) to query/check
to check whether the inputted username is available. -
Recall that you can prevent submission of a form with
preventDefault
, and you can programmatically induce submission of a form (once validated by you) withsubmit
.
personal touch
Implement at least one personal touch of your choice:
-
Allow users to change their passwords.
-
Allow users to add additional cash to their account.
-
Allow users to buy more shares or sell shares of stocks they already own via
index
itself, without having to type stocks' symbols manually. -
Require users' passwords to have some number of letters, numbers, and/or symbols.
-
Implement some other feature of comparable scope.
Walkthroughs
Instead of pwd_context.encrypt
, which Zamyla mentions, be sure to use generate_password_hash
instead.
Note that Zamyla does not discuss /check
, but you should still implement it!
Testing
Be sure to test your web app manually too, as by
-
inputting alpabetical strings into forms when only numbers are expected,
-
inputting zero or negative numbers into forms when only positive numbers are expected,
-
inputting floating-point values into forms when only integers are expected,
-
trying to spend more cash than a user has,
-
trying to sell more shares than a user has,
-
inputting an invalid stock symbol, and
-
including potentially dangerous characters like
'
and;
in SQL queries.
Correctness
check50 cs50/problems/2019/ap/finance
Style
style50 application.py
Staff’s Solution
You’re welcome to stylize your own app differently, but here’s what the staff’s solution looks like!
Feel free to register for an account and play around. Do not use a password that you use on other sites.
It is reasonable to look at the staff’s HTML and CSS.
Hints
-
Within
cs50.SQL
is anexecute
method whose first argument should be astr
of SQL. If thatstr
contains named parameters to which values should be bound, those values can be provided as additional named parameters toexecute
. See the implementation oflogin
for one such example. The return value ofexecute
is as follows:-
If
str
is aSELECT
, thenexecute
returns alist
of zero or moredict
objects, inside of which are keys and values representing a table’s fields and cells, respectively. -
If
str
is anINSERT
, and the table into which data was inserted contains an autoincrementingPRIMARY KEY
, thenexecute
returns the value of the newly inserted row’s primary key. -
If
str
is aDELETE
or anUPDATE
, thenexecute
returns the number of rows deleted or updated bystr
.
If an
INSERT
orUPDATE
would violate some constraint (e.g., aUNIQUE
index), thenexecute
returnsNone
. In cases of error,execute
raises aRuntimeError
. -
-
Recall that
cs50.SQL
will log to your terminal window any queries that you execute viaexecute
(so that you can confirm whether they’re as intended). -
Be sure to use named bind parameters (i.e., a paramstyle of
named
) when calling CS50’sexecute
method, a laWHERE name=:name
. Do not use f-strings,format
, or+
(i.e., concatenation), lest you risk a SQL injection attack. -
If (and only if) already comfortable with SQL, you’re welcome to use SQLAlchemy Core or Flask-SQLAlchemy (i.e., SQLAlchemy ORM) instead of
cs50.SQL
. -
You’re welcome to add additional static files to
static/
. -
Odds are you’ll want to consult Jinja’s documentation when implementing your templates.
-
It is reasonable to ask others to try out (and try to trigger errors in) your site. Via Share in CS50 IDE’s top-right corner can you share your Application by making it Public. Take care not to share your Editor, which would provide access to your Python code and SQLite database.
-
You’re welcome to alter the aesthetics of the sites, as via
FAQs
ImportError: No module named 'application'
By default, flask
looks for a file called application.py
in your current working directory (because we’ve configured the value of FLASK_APP
, an environment variable, to be application.py
). If seeing this error, odds are you’ve run flask
in the wrong directory!
OSError: [Errno 98] Address already in use
If, upon running flask
, you see this error, odds are you (still) have flask
running in another tab. Be sure to kill that other process, as with ctrl-c, before starting flask
again. If you haven’t any such other tab, execute fuser -k 8080/tcp
to kill any processes that are (still) listening on TCP port 8080.
check50 ran into an error while running checks!
If, upon running check50
, you see this error, odds are you have a bug in your code somewhere! Open up the submit.cs50.io link produced by check50
to see a detailed traceback to help you debug!
check50 is taking longer than normal!
If you see this, make sure to remove or comment out any print statements from application.py and try check50
again.
How to Submit
Step 1 of 2
Ensure you have all of the files below:
-
application.py
-
finance.db
-
helpers.py
-
questions.txt
Be sure that each of your files are in ~/chapter8/finance
, as with:
cd ~/chapter8/finance
ls
If any file is not in ~/chapter8/finance
, move it into that directory, as via mv
(or via CS50 IDE’s lefthand file browser).
Step 2 of 2
To submit finance
, execute
+
cd ~/chapter8/finance/
submit50 cs50/problems/2019/ap/finance
+ inputting your GitHub username and GitHub password as prompted.
If you run into any trouble, email sysadmins@cs50.harvard.edu!
You may resubmit any problem as many times as you’d like.
Your submission should be graded for correctness within 2 minutes, at which point your score will appear at submit.cs50.io!
This was C$50 Finance.