以下のように、ユーザが株式を 「購入」 および 「売却」 できるウェブサイトを実装します。
背景
株 (会社の株式) を売買することの意味がよくわからない場合は、ここでチュートリアルを参照してください。
株のポートフォリオを管理するウェブアプリ、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 viaSELECT
withWHERE
). (WHERE
を使用したSELECT
のように) 検索に使用するフィールドに (UNIQUE
以外の) インデックスを定義します。
- ユーザが現在の価格で株式数を購入できない場合は、購入を完了せずにapologyを表示します。
- 競合状態 (トランザクション) を心配する必要はありません。
buy
を正しく実装すると、phpLiteAdminまたはsqlite3
を介して新しいテーブルでユーザの購入を確認できるようになります。
index
現在ログインしているユーザについて、そのユーザが所有している株式、所有株式数、各株式の現在の価格、および各株式の合計 (株価×価格) を要約したHTMLテーブルが表示されるように、index
の実装を完了します。また、ユーザの現在の現金残高と総計 (株式の総額に現金を加えたもの) も表示します。
- 複数の
SELECT
sを実行したい場合があるでしょう。テーブルの実装方法によって、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