<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出這些明細就好。