<style>
html, body, .ui-content {
background-color: #333;
color: #ddd;
}
body > .ui-infobar {
display: none;
}
.ui-view-area > .ui-infobar {
display: block;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
color: #ddd;
}
.markdown-body h1,
.markdown-body h2 {
border-bottom-color: #ffffff69;
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
color: #fff;
}
.markdown-body img {
background-color: transparent;
}
.ui-toc-dropdown .nav>.active:focus>a, .ui-toc-dropdown .nav>.active:hover>a, .ui-toc-dropdown .nav>.active>a {
color: white;
border-left: 2px solid white;
}
.expand-toggle:hover,
.expand-toggle:focus,
.back-to-top:hover,
.back-to-top:focus,
.go-to-bottom:hover,
.go-to-bottom:focus {
color: white;
}
.ui-toc-dropdown {
background-color: #333;
}
.ui-toc-label.btn {
background-color: #191919;
color: white;
}
.ui-toc-dropdown .nav>li>a:focus,
.ui-toc-dropdown .nav>li>a:hover {
color: white;
border-left: 1px solid white;
}
.markdown-body blockquote {
color: #bcbcbc;
}
.markdown-body table tr {
background-color: #5f5f5f;
}
.markdown-body table tr:nth-child(2n) {
background-color: #4f4f4f;
}
.markdown-body code,
.markdown-body tt {
color: #eee;
background-color: rgba(230, 230, 230, 0.36);
}
a,
.open-files-container li.selected a {
color: #5EB7E0;
}
</style>}
# Python + Flask 虛擬美國股票交易網站 Part7 (執行交易)
###### tags: `CS50` `Python` `Flask`
## 前言
要交易一支股票通常會經過以下幾個步驟
* 查價
* 選擇(買/賣) 以及股數
* 確認交易明細
* 送出交易單
這些function都會寫在 vfinance/bluepirnts/admin.py 內
## 查價
```
@admin_bp.route('/quote', methods=["GET", "POST"])
@login_required
def get_quote():
form = QuoteForm()
if form.validate_on_submit():
symbol = form.symbol.data
quote = lookup(symbol)
if quote:
return redirect(url_for(".show_quote", symbol = symbol))
else:
flash("Cannot find", 'warning')
return redirect_back()
return render_template("admin/quote.html", form = form)
```
```
<form class="form-inline my-2 my-lg-0" method = 'post' action="{{ url_for('admin.get_quote')}}">
{% if form %}
{{form.csrf_token}}
{% endif %}
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" id ='symbol' name = 'symbol'>
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Quote</button>
</form>
```
原本是有做一個查價的頁面,後來覺得直接做在navbar上面比較簡單直觀,不論在哪個頁面想查價都可以直接查,所以就寫在了基模板上面。因為是用form的形式,所以一定要加一個csrf_token,不然會出現報錯;模板使用`action="{{ url_for('admin.get_quote')}}"` 傳回資訊到 `get_quote`函數,對應的視圖函數 `get_quote()` 使用`form.symbol.data` 接收到參數後會接到上一篇說的 `lookup()` 查價功能,如果有查到就會導向下一個`show_quote` 函數,如果沒有就會重新導向現在的頁面,並顯示'Cannot find'。
## 顯示個股資訊
```
@admin_bp.route('/quote/<string:symbol>', methods =["GET","POST"])
@login_required
def show_quote(symbol):
form = QuoteForm()
quote = lookup(symbol)
company = quote["name"]
price = quote["price"]
symbol = quote['symbol']
return render_template("admin/show_quote.html",form = form, company = company, price = price, symbol = symbol)
```
#### vfinance/templates/admon/show_quotes
```
{% extends 'base.html' %}
{% from 'bootstrap/form.html' import render_form %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="container h-100">
<div>
<h1>Search for: {{symbol}}</h1>
<h1>Company: {{ company }}</h1>
<h1>Price: {{ price }}</h1>
<h1>
{% if symbol %}
<a href = "{{ url_for('admin.get_trade', symbol = symbol) }}">
Trade {{symbol}}
</a>
<a href="{{url_for('admin.add_to_watchlist', symbol = symbol)}}">+Watchlist</a>
{% endif %}
</h1>
</div>
{% endblock %}
{% block footer %}{% endblock %}
```
show_quote是用get的方式接收到參數(symbol),接受到之後會馬上再去執行lookup查詢個股資訊,並傳回所需要的值,最後渲染到對應的模板`show_quote.html` 上。
在`show_quote.html` 裡除了顯示個股相關數據之外,還有兩個連結,一個是要連到交易的頁面 `get_trade`,一個是要把這個個股加入觀察清單 `add_to_watchlist` 。
## 交易頁面
```
@admin_bp.route("/trade/<string:symbol>", methods=['GET', 'POST'])
@login_required
def get_trade(symbol):
form = TradeForm()
quote = lookup(symbol)
if form.validate_on_submit():
symbol = form.symbol.data
quantity = form.quantity.data
price = form.price.data
action = form.action.data
return redirect(url_for('.review_order', symbol = symbol, quantity= quantity, price = price, action = action))
form.symbol.data = symbol
form.price.data = quote['price']
return render_template("admin/get_trade.html", form = form)
```
vfinance/templates/admin/get_trade:
```
{% extends 'base.html' %}
{% from 'bootstrap/form.html' import render_form %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="row h-100 page-header justify-content-center align-items-center">
<h1>Trade Center</h1>
</div>
<div class="row h-100 justify-content-center align-items-center">
{{ render_form(form, extra_classes='col-6') }}
</div>
</div>
{% endblock %}
{% block footer %}{% endblock %}
```
進到交易頁面會先用bootstrap的 `render_form` 功能渲染之前做的tradeForm,在tradeForm的symbol以及price欄位中,用`form.symbol.data = symbol` 以及 `form.price.date = quote['price']` 先放在裡面,剩下的quantity 以及 action 讓用戶自己選擇股數以及是要買還是賣。
## 檢視交易明細頁面
在交易頁面選定好要買或賣一隻個股以及所需的股數後,按下送出,就會自動導向到review_order頁面。
但在檢視交易明細頁面中有幾個地方需要檢查
* 買股票 `if action == 'Buy'`
買股票比較單純,只要檢查帳戶的錢夠不夠交易就好了,因此會先藉由查詢用戶有多少現金 `current_user.cash`以及總購買股票的價格來做比較,如果錢不夠就會導向前一個頁面,並跳出你沒錢的訊息
`flash("Your order be rejected if you do not have enough cash to cover this closing transaction.", 'danger')`
* 賣股票 `if action == 'Sell'`
賣股票檢查兩個地方,先檢查持股清單是不是空的,如果是空的直接導向前一個頁面並顯示錯誤訊息;再來檢查是不是有足夠的持股可以賣出,如果持股數量不足也會報錯
```
@admin_bp.route('/trade/review', methods=['GET', 'POST'])
@login_required
def review_order():
cash = current_user.cash
symbol = request.args.get('symbol')
price = request.args.get('price')
quantity = request.args.get('quantity')
action = request.args.get('action')
totalprice = float(price)*float(quantity)
able_to_trade = True
stocklist = []
if action == 'Buy':
cash_balance = float(cash) - totalprice
if cash_balance < 0:
able_to_trade = False
flash("Your order be rejected if you do not have enough cash to cover this closing transaction.", 'danger')
return redirect_back()
else:
cash_balance = float(cash) + totalprice
portfolio = Portfolio.query.with_parent(current_user).all()
# check if any existed portfolio, if no, return direct_back
if portfolio:
for company in portfolio:
stocklist.append(company.symbol)
if symbol in stocklist:
id = company.id
stock = Portfolio.query.get_or_404(id)
own_quantity = float(stock.quantity)
if float(quantity) > own_quantity:
able_to_trade = False
flash("Your share isn't enough for the trade. the order was rejected", 'danger')
return redirect_back()
else:
flash("You dont have any share yet", 'danger')
return redirect_back()
else:
able_to_trade = False
flash("You portfolio is empty", 'danger')
return redirect_back()
return render_template("admin/review_order.html",able_to_trade = able_to_trade, action = action, symbol = symbol, price= price, quantity = quantity, cash = cash, totalprice=totalprice, cash_balance=cash_balance)
```
vfinance/template/admin/review_order.html
```
{% extends 'base.html' %}
{% from 'bootstrap/form.html' import render_form %}
{% block title %}Login{% endblock %}
{% block content %}
<div >
<div>
<h1>{{ symbol }}: {{ price }}</h1>
</div>
<div>
<h3><strong>{{ action }} {{ quantity }}</strong>
{% if request.args.get('quantity') == '1' %}
share
{% else %}
shares
{% endif %}
<strong>{{ symbol }}</strong>
</h3>
<h3>Estimated trade amount: {{ totalprice }}</h3>
<h3>Commission: $0.00</h3>
<h3>cash: {{ cash }}</h3>
<h3>Buying power after trade: {{cash_balance}}</h3>
<form class="inline" method="post"
action="{{ url_for('admin.place_order', symbol = symbol, price = price, totalprice = totalprice, name = name, action = action, quantity = quantity, able_to_trade = able_to_trade,next=request.full_path) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-success btn-sm">Approve</button>
</form>
</div>
</div>
{% endblock %}
{% block footer %}{% endblock %}
```
## 送出交易單
```
@admin_bp.route('/trade/placeorder', methods=['GET', 'POST'])
@login_required
def place_order():
# get trade condition
able_to_trade = request.args.get('able_to_trade')
action = request.args.get("action")
if able_to_trade:
# get original cash and position
cash = float(current_user.cash)
# position = float(current_user.position)
# update cash and position back to database
if action =="Buy":
totalprice = float(request.args.get('totalprice'))
current_user.cash = cash - totalprice
# current_user.position = position + totalprice
else:
totalprice = float(request.args.get('totalprice'))
current_user.cash = cash + totalprice
# current_user.position = position - totalprice
#update trade_history
symbol = request.args.get('symbol')
name = lookup(symbol)['name']
price = request.args.get('price')
quantity = request.args.get('quantity')
trade_history = TradeHistory(symbol = symbol, name = name, price = price, action = action, quantity = quantity)
db.session.add(trade_history)
current_user.trade_history.append(trade_history)
# update portfolio
portfolio = Portfolio.query.with_parent(current_user).all()
if portfolio:
# check if the stock already existed
for company in portfolio:
# if exists update the averge pruchase price and quantity
if symbol == company.symbol:
# get purchase price and symbol
id = company.id
stock = Portfolio.query.get_or_404(id)
purchase_price = float(stock.purchase_price)
own_quantity = float(stock.quantity)
# caculate new purchase price and quantity
if action =="Buy":
new_purchase_price = (purchase_price * own_quantity + float(quantity) * float(price))/(own_quantity + float(quantity))
new_own_quantity = own_quantity + float(quantity)
else:
new_own_quantity = own_quantity - float(quantity)
if new_own_quantity == 0:
db.session.delete(stock)
db.session.commit()
return redirect(url_for('home.index'))
new_purchase_price = (purchase_price*own_quantity - float(quantity) * float(price))/(own_quantity-float(quantity))
stock.purchase_price = new_purchase_price
stock.quantity =new_own_quantity
db.session.commit()
return redirect(url_for("home.index"))
# if not exist in portfolio, create it
new_portfolio = Portfolio(symbol = symbol, name = name, purchase_price = price, quantity = quantity)
current_user.portfolio.append(new_portfolio)
else:
new_portfolio = Portfolio(symbol = symbol, name = name, purchase_price = price, quantity = quantity)
current_user.portfolio.append(new_portfolio)
db.session.commit()
return redirect(url_for("home.index"))
```
用戶review完他的order確定無誤後送出,這時候place_order function就會開始更新用戶的帳戶餘額、交易紀錄、以及持股清單。這樣一來就完成了基本的買賣功能。 之後只要在模板上show出這些明細就好。