以下のように、ユーザが株式を 「購入」 および 「売却」 できるウェブサイトを実装します。

背景
株 (会社の株式) を売買することの意味がよくわからない場合は、ここでチュートリアルを参照してください。
株のポートフォリオを管理するウェブアプリ、C$50 Financeを実装しましょう。このツールを使うと、実際の株の実際の価格やポートフォリオの価値をチェックできるだけでなく、IEXに株の価格を照会することで、株の売買もできます。
実際、IEXではhttps://cloud-sse.iexapis.com/stable/stock/nflx/quote?token=API_KEYのようなURLからAPI (アプリケーション・プログラミング・インターフェース) 経由で株価情報をダウンロードできます。Netflixのシンボル (NFLX) がこのURLに埋め込まれていることに注目してください。これがIEXが返すデータを知る方法です。IEXではAPIキーを使用する必要があるため、このリンクは実際には何もデータを返しませんが (後でもう少し詳しく説明します) 、APIキーを使用すると、次のようなJSON (JavaScript Object Notation) 形式のレスポンスが表示されます。
{
"symbol": "NFLX",
"companyName": "Netflix, Inc.",
"primaryExchange": "NASDAQ",
"calculationPrice": "close",
"open": 317.49,
"openTime": 1564752600327,
"close": 318.83,
"closeTime": 1564776000616,
"high": 319.41,
"low": 311.8,
"latestPrice": 318.83,
"latestSource": "Close",
"latestTime": "August 2, 2019",
"latestUpdate": 1564776000616,
"latestVolume": 6232279,
"iexRealtimePrice": null,
"iexRealtimeSize": null,
"iexLastUpdated": null,
"delayedPrice": 318.83,
"delayedPriceTime": 1564776000616,
"extendedPrice": 319.37,
"extendedChange": 0.54,
"extendedChangePercent": 0.00169,
"extendedPriceTime": 1564876784244,
"previousClose": 319.5,
"previousVolume": 6563156,
"change": -0.67,
"changePercent": -0.0021,
"volume": 6232279,
"iexMarketPercent": null,
"iexVolume": null,
"avgTotalVolume": 7998833,
"iexBidPrice": null,
"iexBidSize": null,
"iexAskPrice": null,
"iexAskSize": null,
"marketCap": 139594933050,
"peRatio": 120.77,
"week52High": 386.79,
"week52Low": 231.23,
"ytdChange": 0.18907500000000002,
"lastTradeTime": 1564776000616
}
中カッコの間に、カンマで区切られたキーと値のペアのリストがあり、各キーと値がコロンで区切られていることに注目してください。
次に、この問題の配布コードを見てみましょう。
コードの配布
ダウンロード
$ wget http://cdn.cs50.net/2020/fall/psets/9/finance/finance.zip
$ unzip finance.zip
$ rm finance.zip
$ cd finance
$ ls
application.py helpers.py static/
finance.db requirements.txt templates/
準備
この課題を開始する前に、IEXのデータを照会できるようにAPIキーを登録する必要があります。これを行うには、次の手順を実行します。
- iexcloud.io/cloud-login#/register/にアクセスしてください。
- 「個人 (Individual)」 アカウントタイプを選択し、電子メールアドレスとパスワードを入力して、 「アカウントの作成 (Create account)」 をクリックします。
- 登録が完了したら、 「Get started for free」 までスクロールし、 「Select Start」 をクリックして無料プランを選択します。
- 確認メールでアカウントを確認したら、https://iexcloud.io/console/tokensにアクセスしてください。
- [Token] カラムの下に表示されているキーをコピーします (
pk_で始まる必要があります) 。 - CS50 IDEのターミナルウィンドウで、次のコマンドを実行します。
$ export API_KEY=value
valueは、=の直前または直後にスペースを入れずに貼り付けた値です。また、後で再度必要になる場合に備えて、その値を文書ドキュメントのどこかに貼り付けることもできます。
実行
Flaskの組み込みWebサーバ (finance/内) を起動します。
$ flask run
flaskが出力するURLにアクセスして、実際の配布コードを確認してください。ですが、まだログインも登録もできません。
CS 50のファイルブラウザでfinance.dbをダブルクリックし、phpLiteAdminで開きます。finance.dbにはusersというテーブルが付属しています。その構造 (スキーマ) を見てみましょう。デフォルトでは、新規ユーザは1万ドルの現金を受け取ります。しかし、 (まだ) ユーザは1人も見当たりません (行として表示されていません)。
コマンドラインを使いたい場合は、phpLiteAdminではなくsqlite3を使用してください。
理解を深める
application.py
application.pyを開きます。ファイルの上には、CS50のSQLモジュールやいくつかのヘルパー関数など、たくさんのインポートがあります。詳細はこの後すぐに解説します。
Flaskを設定した後、あるファイルに変更を加えてもブラウザがそれに気付かないということを避けるため、このファイルがレスポンスのキャッシュを無効にしていること (CS 50 IDEのデフォルトでデバッグモードになっていること) に注意してください。次に、カスタムされた 「filter」 usd関数 (helpers.pyで定義されている) を使用してJinjaを設定する方法に注目してください。この関数を使用すると、値を米ドル (USD) として簡単にフォーマットできます。さらに、セッションをFlaskのデフォルトである (デジタル署名された) cookie内に格納するのではなく、ローカルファイルシステム (ディスク) に格納するようにFlaskを構成しています。次に、CS50のSQLモジュールがfinance.dbを使用するように設定されます。finance.dbはSQLiteデータベースで、その内容については後で説明します。
それ以降はたくさんのルートがありますが、完全に実装されているのはlogin とlogoutの2つだけです。最初にlogin の実装を読んでください。CS 50のライブラリのdb.executeを使ってfinance.dbをクエリしていることに注目してください。また、check_password_hashを使用してユーザのパスワードのハッシュを比較しています。最後に、ユーザのuser_id (INTEGER) をsessionに格納することで、ユーザがログインしたことをloginがどのように 「記憶」 しているかに注目してください。これにより、このファイルのルートのいずれかで、ログインしているユーザ (存在する場合) をチェックできます。一方、logoutでは単純にsessionがクリアされ、ユーザが実質的にログアウトされます。
ほとんどのルートが@login_required (helpers.pyに定義されている関数)で 「修飾」 されていることに注目してください。このデコレーターは、ユーザがこれらのルートのいずれかにアクセスしようとすると、まずloginにリダイレクトされてログインできるようにします。
ほとんどのルートがGETとPOSTをサポートしていることにも注目してください。それでも、ほとんどのルート (今のところ) はまだ実装されていないため、単に 「apology (謝罪)」 を返すようになっています。
helpers.py
次にhelpers.pyを見てみましょう。apologyが実装されていることがわかります。最終的にテンプレートapology.htmlをどのようにレンダリングしているかに注目してください。また、その中で特殊文字を置き換えるために使用する別の関数escapeを定義しています。apologyの中でescape を定義することで、escape をapologyだけにスコープすることができます。つまり、他の関数はこの関数を呼び出すことができません (あるいは呼び出す必要がありません) 。
次の関数はlogin_requiredです。少しわかりにくいかもしれませんが、ある関数が別の関数を返す方法を知りたいと思ったことがある方のために、ここで例を示します。
lookupは、symbol (例:NFLX) を指定して、3つのキーを持つdict 形式で会社の株価を返す関数です。キーはそれぞれ、name、値はstr (会社の名前)、price、値はfloat、およびsymbol、値はstr (株式シンボルが正規化 (大文字化) されたもの)です。これは、シンボルがlookupに渡されたときに大文字・小文字関係なく受け取れるようにするためです。
ファイルの最後にあるusdは、floatをfloatとしてフォーマットする短い関数です (たとえば、1234.56は$1,234.56としてフォーマットされます)。
requirements.txt
次に、requirements.txtを簡単に見てみましょう。このファイルには、このアプリケーションが依存するパッケージが記述されています。
static/
static/の内部にあるstyles.cssにも目を向けてください。そこにCSSが少し記述されているはずです。好きなように編集してください。
templates/
次に、templates/を見てください。login.htmlは基本的には、Bootstrapで様式化されたHTMLフォームです。一方、apology.htmlは、apology (謝罪) のためのテンプレートです。helpers.pyのapologyには2つの引数が必要でした。messageはbottomの値としてrender_templateに渡され、codeはオプション引数として、topの値としてrender_templateに渡されました。これらの値が最終的にどのように使用されているかは、apology.htmlを参照してください。そしてこれが理由です。
最後はlayout.htmlです。通常よりもやや大きいファイルですが、それは主に、同じくBootstrapをベースにした、モバイルで使いやすい 「ナビバー」 (ナビゲーションバー) があるからです。ブロックmainがどのように定義されているかに注目してください。このブロックの内部には、テンプレート (apology.htmlおよびlogin.htmlを含む) があります。また、Flaskのメッセージ・フラッシュのサポートも含まれているため、あるルートから別のルートにメッセージをリレーしてユーザに表示することができます。
仕様
register
ユーザがフォームを介してアカウントに登録できるように、register の実装を完了します。
- ユーザは
usernameというnameに実装されたテキストフィールドに、ユーザ名を入力する必要があります。ユーザの入力が空白の場合、またはユーザ名がすでに存在する場合は、apologyを表示します。 - ユーザが、
passwordというnameに実装されたテキストフィールドに、パスワードを入力し、次にconfirmationというnameに実装されたテキストフィールドに、同じパスワードを再度入力するように要求します。入力が空白の場合、またはパスワードが一致しない場合は、apologyを表示します。 - ユーザの入力を
POST経由で/registerに送信します。 - 新しいユーザを
usersにINSERTし、パスワード自体ではなく、ユーザのパスワードのハッシュを保存します。generate_password_hashでユーザのパスワードをハッシュしましょう。login.htmlによく似た新しいテンプレート (例:register.html) を作成する必要があるかもしれません。 - ユーザーが登録されると、ユーザーに自動的にログインするか、ユーザーが自分でログインできるページにユーザーを誘導することができます。
register を正しく実装したら、アカウントを登録してログインできるようになります (login とlogout はすでに機能しています) 。また、phpLiteAdminまたはsqlite3を使用して行を表示できます。
quote
ユーザが株式の現在の価格を検索できるように、quoteの実装を完了します。
- ユーザが、
symbolというnameに実装されたテキストフィールドに、株式シンボルを入力する必要があります。 - ユーザの入力を
POST経由で/quoteに送信します。 - 2つの新しいテンプレートを作成することができます (例:
quote.htmlおよびquoted.html)。ユーザがGETを介して/quoteにアクセスした場合、これらのテンプレートの1つをレンダリングします。テンプレートの内部は、POSTを介して/quoteに送信するHTMLフォームである必要があります。POSTに応答して、quoteは2番目のテンプレートをレンダリングし、その中にlookupから1つ以上の値を埋め込むことができます。
buy
ユーザが株を購入できるように、buyの実装を完了します。
- ユーザが、
symbolというnameに実装されたテキストフィールドに、株式シンボルを入力する必要があります。入力が空白の場合、またはシンボルが存在しない場合は (lookupの戻り値に従って) 、apologyを表示します。 - ユーザが、
sharesというnameに実装されたテキストフィールドに、株式数を入力する必要があります。入力が正の整数でない場合は、apologyを表示します。 - ユーザの入力を
POST経由で/buyに送信します。 lookupを呼び出して株の現在の価格を調べる必要があるでしょう。- ユーザが現在どれだけの現金を持っているかを
SELECTする必要があるでしょう。 finance.dbに1つ以上の新しいテーブルを追加し、それを介して購入を追跡します。誰がいつ、どの価格で何を買ったかがわかるように、十分な情報を保存します。- 適切なSQLiteタイプを使用します。
- 一意である必要があるフィールドに
UNIQUEインデックスを定義します。
- Define (non-
UNIQUE) indexes on any fields via which you will search (as viaSELECTwithWHERE). (WHEREを使用したSELECTのように) 検索に使用するフィールドに (UNIQUE以外の) インデックスを定義します。
- ユーザが現在の価格で株式数を購入できない場合は、購入を完了せずにapologyを表示します。
- 競合状態 (トランザクション) を心配する必要はありません。
buyを正しく実装すると、phpLiteAdminまたはsqlite3を介して新しいテーブルでユーザの購入を確認できるようになります。
index
現在ログインしているユーザについて、そのユーザが所有している株式、所有株式数、各株式の現在の価格、および各株式の合計 (株価×価格) を要約したHTMLテーブルが表示されるように、indexの実装を完了します。また、ユーザの現在の現金残高と総計 (株式の総額に現金を加えたもの) も表示します。
- 複数の
SELECTsを実行したい場合があるでしょう。テーブルの実装方法によって、GROUP BY、HAVING、SUM、WHEREが必要になる場合があります。 - 多くの場合、各株式の
lookup呼び出す必要があります。る
sell
ユーザが (所有する) 株式を売却できるように、sellの実装を完了します。
- ユーザが株式シンボルを入力する必要があります。この記号は、
symbolというnameのselectメニューとして実装されます。ユーザが株式を選択しなかった場合、またはユーザがその株式の株式を所有していない場合 (何らかの理由で保有している株式が売却できない場合も) は、apologyを表示します。 - ユーザが、
sharesというnameに実装されたテキストフィールドに、株式数を入力する必要があります。入力が正の整数でない場合、またはユーザが保有数以上の株式数を入力した場合は、apologyを表示します。 POSTを通じてユーザの入力を/sellに送信します。- 競合状態 (トランザクション) を心配する必要はありません。
history
historyの実装を完了して、ユーザのすべてのトランザクションを要約したHTMLテーブルを表示し、すべての売買を行ごとにリストします。
- 各行について、株式が売買されたかどうかを明確にし、株式シンボル、 (売買) 価格、売買株式数、取引が行われた日時を含めます。
buy用に作成したテーブルを変更したり、テーブルを追加したりする必要がある場合があります。重複を最小限に抑えるようにしてください。
パーソナリティ機能
少なくとも1つのオリジナルな機能を選択して実装します。
- ユーザがパスワードを変更できるようにします。
- ユーザが自分のアカウントに現金を追加できるようにします。
- 株式の銘柄シンボルを手動で入力しなくても、ユーザがすでに所有している株式を
index自体を介して購入したり売却したりできるようにします。 - ユーザのパスワードには、いくつかの文字、数字、記号を含める必要があるようにします。
- 同等のスコープで他の機能を実装します。
ウォークスルー
テスト
check50でコードをテストするには、以下を実行します。
$ check50 cs50/problems/2021/x/finance
check50はプログラム全体をテストすることに注意してください。必要なすべての関数を完了する前に実行すると、テストしたい関数が実際には正しくても、他の関数に依存する関数のエラーがレポートされる場合があります。
以下のように、ウェブアプリも手動でテストしてください。
- 数字のみが想定される場合に、アルファベット文字列をフォームに入力する。
- 正の数のみが想定される場合に、フォームに0または負の数を入力する。
- 整数のみが想定される場合に、浮動小数点値をフォームに入力する。
- ユーザが持っているよりも多くの現金を使用する。
- ユーザが持っている以上の株を売却する。
- 無効な株式シンボルを入力する。
'や;などの潜在的に危険な文字を含むSQLクエリを使用する。
以下を実行し、style50を使用してPythonファイルのスタイルを評価します。
style50 *.py
スタッフの解法
オリジナリティのあるアプリをスタイルするのは大歓迎ですが、スタッフのソリューションも見てみましょう。
アカウントを登録して、自由に触ってください。他のサイトで使用するパスワードは使用しないでください。
スタッフのHTMLとCSSを見るのは理にかなっています。
ヒント
- 値をUSドルの値 (セントは小数点以下2桁まで表示) としてフォーマットするには、Jinjaテンプレートで
usdフィルタを使用します (値を{{ value }}の代わりに{{ value | usd }}として出力します) 。 cs50.SQL内では、最初の引数がSQLのstrであるexecuteメソッドです。そのstrに、値をバインドする疑問符パラメータが含まれている場合は、それらの値をexecuteする追加の名前付きパラメータとして指定できます。このような例については、loginの実装を参照してください。executeの戻り値は次のとおりです。strがSELECTの場合、executeは0個以上のdictオブジェクトのリストlistを返します。その中にはテーブルのフィールドとセルを表すキーと値が含まれます。
strがINSERTであり、データが挿入されたテーブルに自動インクリメントするPRIMARY KEYが含まれている場合、executeは新しく挿入された行の主キーの値を返します。
strがDELETEまたはUPDATEの場合、executeはstrによって削除または更新された行数を返します。
cs50.SQLは、executeによって実行されたすべてのクエリをターミナルウィンドウに記録します (これにより、クエリが意図したとおりであるかどうかを確認できます) 。- CS50の
executeメソッドを呼び出す場合は、WHERE ?のように必ず疑問符付きのパラメータ (namedのparamstyle) を使用してください。SQLインジェクション攻撃のリスクを避けるため、f-string、format、+(連結)は使用しないでください。 - すでにSQLに慣れている場合 (慣れている場合に限ります) は、
cs50.SQLの代わりにSQLAlchemy CoreまたはFlask-SQLAlchemy (SQLAlchemy ORM) を使用してかまいません。 - staticファイルを
static/に追加することもできます。 - テンプレートを実装する際には、 Jinjaのドキュメントを参照することをお勧めします。
- 自分のサイトを試してみる (そしてエラーを引き起こす) ように他の人に頼むのは理にかなっています。
- 以下のようなサイトで美観を変えるのは大歓迎です。
- https://bootswatch.com/
- https://getbootstrap.com/docs/4.1/content/
- https://getbootstrap.com/docs/4.1/components/
- https://memegen.link/
- FlaskのドキュメントとJinjaのドキュメントが役に立つかもしれません。
FAQ
ImportError: No module named ‘application’ (‘application’という名前のモジュールがありません)
デフォルトでは、flaskはカレントワーキングディレクトリでapplication.pyというファイルを探します (環境変数FLASK_APPの値をapplication.pyに設定したためです)。このエラーが表示された場合は、間違ったディレクトリでflaskを実行した可能性があります。
OSError: [Errno 98] Address already in use (アドレスはすでに使用されています)
このエラーがflaskの実行中に表示された場合は、別のタブでflaskが実行されている可能性があります。ctrl-cなどで他のプロセスを終了してから、再度flaskを起動してください。そのようなタブがない場合は、fuser -k 8080/tcpを実行して、TCPポート8080で (まだ) 待機しているプロセスを終了します。
提出方法
次のコマンドを実行し、GitHubのユーザ名とパスワードを入力してログインします。セキュリティのため、パスワードには実際の文字ではなくアスタリスク (*) が表示されます。
submit50 cs50/problems/2021/x/finance