commit
777650df12
24 changed files with 2912 additions and 0 deletions
@ -0,0 +1,13 @@ |
|||||||
|
# Byte-compiled / optimized / DLL files |
||||||
|
.ipynb_checkpoints/ |
||||||
|
__pycache__/ |
||||||
|
.DS_Store |
||||||
|
|
||||||
|
tpm/ |
||||||
|
|
||||||
|
# Distribution / packaging |
||||||
|
main.old.py |
||||||
|
postgres_runner.py |
||||||
|
*.ipynb |
||||||
|
tickers_sorted_tw.py |
||||||
|
tickers_sorted.py |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,387 @@ |
|||||||
|
from flask import Flask, render_template, request, redirect, url_for, g, session, flash, jsonify |
||||||
|
# from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user |
||||||
|
from markupsafe import escape |
||||||
|
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash |
||||||
|
from datetime import datetime, date, timedelta |
||||||
|
|
||||||
|
|
||||||
|
import os |
||||||
|
import json |
||||||
|
import time |
||||||
|
import random |
||||||
|
import string |
||||||
|
import numpy as np |
||||||
|
import pandas as pd |
||||||
|
import psycopg2 |
||||||
|
import plotly |
||||||
|
import plotly.express as px |
||||||
|
from portfolio_builder import MVO |
||||||
|
pd.options.plotting.backend = "plotly" |
||||||
|
|
||||||
|
# PARAMETERS |
||||||
|
CONFIGS = { |
||||||
|
"ENV": "development", |
||||||
|
"DEBUG": True, |
||||||
|
"SECRET_KEY": os.urandom(30), # Set the secret key for session authentication |
||||||
|
"PERMANENT_SESSION_LIFETIME": timedelta(minutes=60) |
||||||
|
} |
||||||
|
SQL_CONFIG = { |
||||||
|
'database': "tpm", |
||||||
|
'user': "hsienchen", |
||||||
|
'host': "127.0.0.1", |
||||||
|
'port': "5432" |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__) |
||||||
|
app.config.from_mapping(CONFIGS) |
||||||
|
# Login Manager, Flask_Login Stuff |
||||||
|
# login_manager = LoginManager() |
||||||
|
# login_manager.init_app(app) |
||||||
|
# login_manager.login_view = 'login' |
||||||
|
def login_required(): |
||||||
|
if not 'username' in session: |
||||||
|
return False |
||||||
|
else: |
||||||
|
return True |
||||||
|
def get_stock(conn, stock_list, tw): |
||||||
|
## Query DB |
||||||
|
if tw==1: |
||||||
|
sql="SELECT ticker, date, price, return FROM stock_price_tw where ticker = ANY(%s);" |
||||||
|
with conn: |
||||||
|
with conn.cursor() as curs: |
||||||
|
curs.execute(sql, (stock_list, )) |
||||||
|
# print(curs.mogrify(sql, (stock_list,))) |
||||||
|
data= curs.fetchall() |
||||||
|
else: |
||||||
|
sql1="SELECT ticker, date, price, return FROM stock_price where ticker = ANY(%s)" |
||||||
|
sql2="SELECT ticker, date, price, return FROM stock_price_tw where ticker = ANY(%s) ;" |
||||||
|
with conn: |
||||||
|
with conn.cursor() as curs: |
||||||
|
curs.execute(sql1, (stock_list,)) |
||||||
|
data_us= curs.fetchall() |
||||||
|
curs.execute(sql2, (stock_list,)) |
||||||
|
data_tw= curs.fetchall() |
||||||
|
data = data_us+data_tw |
||||||
|
dfStock = pd.DataFrame(data, columns=['ticker', 'date', 'price', 'return']) |
||||||
|
dfStock['date'] = pd.to_datetime(dfStock['date']) |
||||||
|
dfStock = dfStock.drop_duplicates() |
||||||
|
g = dfStock.groupby('ticker') |
||||||
|
port = pd.concat([g.get_group(t).set_index('date')['price'] for t in stock_list], axis=1, join='inner') |
||||||
|
port.columns=stock_list |
||||||
|
return port |
||||||
|
|
||||||
|
|
||||||
|
# Define the route for the index pages |
||||||
|
@app.route('/') |
||||||
|
def index(): |
||||||
|
return render_template('base.html') |
||||||
|
|
||||||
|
# Login Page |
||||||
|
@app.route('/login') |
||||||
|
def login(): |
||||||
|
# for key in session: |
||||||
|
# print(key, session[key]) |
||||||
|
# print(session.get('username'), session['username'], session.get('username') and session['username']) |
||||||
|
return render_template('login.html') |
||||||
|
@app.route('/login', methods=['POST']) |
||||||
|
def login_post(): |
||||||
|
# Get the username and password from the form |
||||||
|
username = request.form.get('username') |
||||||
|
password = request.form.get('password') |
||||||
|
print(username, password) |
||||||
|
|
||||||
|
|
||||||
|
## Connect to the database |
||||||
|
conn = psycopg2.connect(**SQL_CONFIG) |
||||||
|
with conn: |
||||||
|
with conn.cursor() as curs: |
||||||
|
curs.execute("select * from users where username = %s;", (username, )) |
||||||
|
data = curs.fetchone() |
||||||
|
conn.close() |
||||||
|
|
||||||
|
# Authentication |
||||||
|
if (data is None) or (username is None) or (password is None): |
||||||
|
flash('使用者代號不對或密碼不對,請再試一次。', 'danger') |
||||||
|
return render_template('login.html') |
||||||
|
elif check_password_hash(data[2], password): |
||||||
|
session['username'] = username |
||||||
|
session['user_id'] = data[0] |
||||||
|
session['privilege'] = data[-1] |
||||||
|
session['update_freq'] = 100 |
||||||
|
session['lastCreateTime'] = time.time() |
||||||
|
session['tw'] = 1 |
||||||
|
return redirect(url_for('index')) |
||||||
|
else: |
||||||
|
flash('使用者代號不對或密碼不對,請再試一次。', 'danger') |
||||||
|
return render_template('login.html') |
||||||
|
|
||||||
|
# Registration Page |
||||||
|
@app.route('/registration') |
||||||
|
def registration(): |
||||||
|
if login_required(): |
||||||
|
return redirect(url_for('index')) |
||||||
|
return render_template('registration.html') |
||||||
|
@app.route('/registration', methods=['POST']) |
||||||
|
def registration_post(): |
||||||
|
# Get the username and password from the form |
||||||
|
username = request.form.get('username') |
||||||
|
password = request.form.get('password') |
||||||
|
rep_password = request.form.get('rep-password') |
||||||
|
# check password |
||||||
|
if not password is None and password == rep_password: |
||||||
|
print(username, password) |
||||||
|
conn = psycopg2.connect(**SQL_CONFIG) |
||||||
|
## Connect to the database |
||||||
|
with conn.cursor() as curs: |
||||||
|
curs.execute("select * from users where username = %s;", (username, )) |
||||||
|
data = curs.fetchone() |
||||||
|
if data is None: |
||||||
|
with conn: |
||||||
|
with conn.cursor() as curs: |
||||||
|
curs.execute("insert into users (username, password) values (%s, %s);", (username, generate_password_hash(password))) |
||||||
|
# conn.commit() |
||||||
|
else: |
||||||
|
flash('使用者已存在。', 'warning') |
||||||
|
return redirect(url_for('login')) |
||||||
|
conn.close() |
||||||
|
name = username.split('@')[0] |
||||||
|
flash(f'註冊成功! 歡迎您, {name}。', 'success') |
||||||
|
return redirect(url_for('login')) |
||||||
|
else: |
||||||
|
flash('密碼不符合,請再次輸入。', 'warning') |
||||||
|
return redirect(url_for('registration')) |
||||||
|
|
||||||
|
# Logout Page |
||||||
|
@app.route('/logout', methods=['GET']) |
||||||
|
def logout(): |
||||||
|
if login_required(): |
||||||
|
pass |
||||||
|
else: |
||||||
|
flash('請先登入。', 'warning') |
||||||
|
return redirect(url_for('login')) |
||||||
|
if 'username' in session: |
||||||
|
# for key in list(session.keys()): |
||||||
|
# print(key, session[key]) |
||||||
|
# for key in list(session.keys()): |
||||||
|
# session.pop(key) |
||||||
|
session.clear() |
||||||
|
return redirect(url_for('index')) |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/strategy') |
||||||
|
def strategy(): |
||||||
|
if login_required(): |
||||||
|
pass |
||||||
|
else: |
||||||
|
flash('使用投組功能請先登入。', 'warning') |
||||||
|
return redirect(url_for('login')) |
||||||
|
session['tw'] = 0 |
||||||
|
# Load Assets |
||||||
|
with open('assets_tw.json') as f: |
||||||
|
data_tw = json.load(f) |
||||||
|
with open('assets_us.json') as f: |
||||||
|
data = json.load(f) |
||||||
|
|
||||||
|
print(request.args.get('data'), 666) |
||||||
|
return render_template('strategy_tw.html', stockOpts={**data, **data_tw}) |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/strategy_tw') |
||||||
|
def strategy_tw(): |
||||||
|
if login_required(): |
||||||
|
pass |
||||||
|
else: |
||||||
|
flash('使用投組功能請先登入。', 'warning') |
||||||
|
return redirect(url_for('login')) |
||||||
|
session['tw'] = 1 |
||||||
|
# Load Assets |
||||||
|
with open('assets_tw.json') as f: |
||||||
|
data = json.load(f) |
||||||
|
|
||||||
|
print(request.args.get('data'), 666) |
||||||
|
return render_template('strategy_tw.html', stockOpts=data) |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/postStock', methods=['POST']) |
||||||
|
def submit_stock_list(): |
||||||
|
if login_required(): |
||||||
|
pass |
||||||
|
else: |
||||||
|
print('NOT LOGIN!!') |
||||||
|
return redirect(url_for('index')) |
||||||
|
if not 'tw' in session: |
||||||
|
return redirect(url_for('index')) |
||||||
|
# Update Session |
||||||
|
print("-"*10, "UPDATE ASSET", "-"*10) |
||||||
|
if session['update_freq']==0: |
||||||
|
print('update to frquent!') |
||||||
|
return 'update to frquent!' |
||||||
|
else: |
||||||
|
session['update_freq']-=1 |
||||||
|
flash('Looks like you have changed your name!', 'warning') |
||||||
|
stock_list = request.form.get('stockList') # this is string |
||||||
|
stock_list = json.loads(stock_list) # Load stock_list as list |
||||||
|
session['currStockList'] = stock_list |
||||||
|
|
||||||
|
## Query DB |
||||||
|
conn = psycopg2.connect(**SQL_CONFIG) |
||||||
|
port = get_stock(conn, stock_list, session['tw']) |
||||||
|
conn.close() |
||||||
|
|
||||||
|
fig = port.plot(title = 'Assets in portfolio', |
||||||
|
labels=dict(index="Date", value="Price", variable="Assets")) |
||||||
|
fig['layout'] = dict( |
||||||
|
autosize=True, |
||||||
|
legend={'title': {'text': 'Assets'}, 'tracegroupgap': 0}, |
||||||
|
title= {'text': 'Assets in portfolio'}, |
||||||
|
xaxis= {'anchor': 'y', 'domain': [0.0, 1.0], 'title': {'text': 'Date'}}, |
||||||
|
yaxis= {'anchor': 'x', 'domain': [0.0, 1.0], 'title': {'text': 'Price'}} |
||||||
|
) |
||||||
|
fig.update_layout(legend=dict( |
||||||
|
yanchor="top", |
||||||
|
y=0.99, |
||||||
|
xanchor="left", |
||||||
|
x=0.01 |
||||||
|
)) |
||||||
|
|
||||||
|
print(type(stock_list)) |
||||||
|
|
||||||
|
# 序列化 |
||||||
|
graphJSON = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) |
||||||
|
|
||||||
|
|
||||||
|
for key in request.form: |
||||||
|
print(key, request.form[key]) |
||||||
|
# Do something with the stock list heres |
||||||
|
return graphJSON |
||||||
|
# return jsonify({'message': 'Stock list received successfully, NOOOOO'}) |
||||||
|
|
||||||
|
@app.route('/postPort', methods=['POST']) |
||||||
|
def buildPort(): |
||||||
|
# Login Required |
||||||
|
if login_required(): |
||||||
|
pass |
||||||
|
else: |
||||||
|
print('NOT LOGIN!!') |
||||||
|
return redirect(url_for('index')) |
||||||
|
if not 'tw' in session: |
||||||
|
return redirect(url_for('index')) |
||||||
|
# Stop frequently building strategy |
||||||
|
if time.time() - session['lastCreateTime'] < 20: |
||||||
|
print("UNTIL: ", time.time()-session['lastCreateTime']) |
||||||
|
return jsonify({'mes': '投資組合建立時間間隔(或與登入時間間隔)必須大於60秒!'}) |
||||||
|
session['lastCreateTime'] = time.time() |
||||||
|
|
||||||
|
|
||||||
|
print("-"*10) |
||||||
|
for key in request.form: |
||||||
|
print(key, request.form[key], type(request.form[key])) |
||||||
|
|
||||||
|
# Portfolio Info |
||||||
|
name = request.form.get('name') |
||||||
|
if name == '': |
||||||
|
prefix=''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) |
||||||
|
name= prefix + f"-{round(time.time()%100, 2)}" |
||||||
|
comp = request.form.get('comp') |
||||||
|
ts = int(request.form.get('ts')) |
||||||
|
ts = datetime.fromtimestamp(ts/1000) |
||||||
|
role = request.form.get('role') |
||||||
|
ratio = float(request.form.get('ratio')) |
||||||
|
comment = request.form.get('comment') |
||||||
|
stock_list = json.loads(request.form.get('stockList')) |
||||||
|
|
||||||
|
# Algorithm MVO |
||||||
|
print("-"*10) |
||||||
|
print("Enter Algorithms") |
||||||
|
print("-"*10) |
||||||
|
# time.sleep(20) |
||||||
|
# Query DB |
||||||
|
market_asset = '0050.TW' if session['tw']==1 else 'SPY' |
||||||
|
conn = psycopg2.connect(**SQL_CONFIG) |
||||||
|
if 'market_asset' in stock_list: |
||||||
|
port = get_stock(conn, stock_list, session['tw']) |
||||||
|
market = port[market_asset] |
||||||
|
else: |
||||||
|
port = get_stock(conn, stock_list+[market_asset], session['tw']) |
||||||
|
market = port[market_asset] |
||||||
|
port = port[stock_list] |
||||||
|
|
||||||
|
length, num = port.shape |
||||||
|
tsize = int(length*ratio) |
||||||
|
# time label |
||||||
|
train_label = port.index[1:][:tsize] |
||||||
|
test_label = port.index[1:][tsize:] |
||||||
|
|
||||||
|
# data |
||||||
|
data_return = port.pct_change().dropna().to_numpy() |
||||||
|
market_return = market.pct_change().dropna().to_numpy() |
||||||
|
train = data_return[:tsize, :] |
||||||
|
test = data_return[tsize:, :] |
||||||
|
train_market = market_return[:tsize] |
||||||
|
test_market = market_return[tsize:] |
||||||
|
|
||||||
|
# optimization |
||||||
|
sol = MVO.opt(train, role=role) |
||||||
|
train_info = MVO.portfolio_info(sol, train, train_market) |
||||||
|
test_info = MVO.portfolio_info(sol, test, test_market) |
||||||
|
# print(sol, train_info, test_info) |
||||||
|
print("-"*10) |
||||||
|
print(ts, name, session.get('username'), comp, |
||||||
|
role, test_info['annual_ret'], test_info['vol'], test_info['mdd'], test_info['annual_sr'], |
||||||
|
test_info['beta'], test_info['alpha'], test_info['var10'], test_info['R2'], True, comment, stock_list, list(sol), sep=", ") |
||||||
|
print("-"*10) |
||||||
|
data = (ts, name, session.get('username'), comp, |
||||||
|
role, ratio, test_info['annual_ret'], test_info['vol'], test_info['mdd'], test_info['annual_sr'], |
||||||
|
test_info['beta'], test_info['alpha'], test_info['var10'], test_info['R2'], True, comment, stock_list, list(sol)) |
||||||
|
sql='insert into strategy \ |
||||||
|
(date, name, username, competition, role, ratio, annual_ret, vol, mdd, annual_sr, beta, alpha, var10, R2, tw, comment, assets, assets_position)\ |
||||||
|
values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s);' |
||||||
|
with conn: |
||||||
|
with conn.cursor() as curs: |
||||||
|
curs.execute(sql, data) |
||||||
|
conn.close() |
||||||
|
print("\n------Write in Success--------\n") |
||||||
|
return jsonify({'mes': '投資組合已完成建立,請至<a href"/">gooooooo</a>查詢分析結果。'}) |
||||||
|
|
||||||
|
@app.route('/custom') |
||||||
|
def custom(): |
||||||
|
if login_required(): |
||||||
|
pass |
||||||
|
else: |
||||||
|
flash('使用投組功能請先登入。', 'warning') |
||||||
|
return redirect(url_for('login')) |
||||||
|
return render_template('custom.html', message='No') |
||||||
|
@app.route('/result') |
||||||
|
def result(): |
||||||
|
if login_required(): |
||||||
|
pass |
||||||
|
else: |
||||||
|
flash('使用投組功能請先登入。', 'warning') |
||||||
|
return redirect(url_for('login')) |
||||||
|
return render_template('result.html') |
||||||
|
@app.route('/result_tw') |
||||||
|
def result_tw(): |
||||||
|
if login_required(): |
||||||
|
pass |
||||||
|
else: |
||||||
|
flash('使用投組功能請先登入。', 'warning') |
||||||
|
return redirect(url_for('login')) |
||||||
|
return render_template('result_tw.html') |
||||||
|
|
||||||
|
# handle login failed |
||||||
|
@app.errorhandler(401) |
||||||
|
def page_not_found(e): |
||||||
|
return Response('<p>Failed</p>') |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
app.run(host='0.0.0.0', port=8000) |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,108 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<title>To-Do List</title> |
||||||
|
<!-- Bootstrap CSS --> |
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="container mt-5"> |
||||||
|
<form id="stock-form"> |
||||||
|
<div class="form-group"> |
||||||
|
<label for="stock-select">Select a stock:</label> |
||||||
|
<select class="form-control" id="stock-select"> |
||||||
|
<option value="AAPL">AAPL (Apple Inc.)</option> |
||||||
|
<option value="GOOG">GOOG (Alphabet Inc.)</option> |
||||||
|
<option value="MSFT">MSFT (Microsoft Corporation)</option> |
||||||
|
<option value="AMZN">AMZN (Amazon.com, Inc.)</option> |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
<button type="submit" class="btn btn-primary">Add</button> |
||||||
|
</form> |
||||||
|
<button type="submit" class="btn btn-primary" id="submit-btn">Add to Watchlist</button> |
||||||
|
<ul class="list-group mt-3" id="stock-list"></ul> |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
<!-- jQuery and Bootstrap JS --> |
||||||
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script> |
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script> |
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script> |
||||||
|
|
||||||
|
<script> |
||||||
|
// Initialize empty stock list |
||||||
|
let stockList = []; |
||||||
|
|
||||||
|
// Cache frequently-used DOM elements |
||||||
|
const $stockForm = $('#stock-form'); |
||||||
|
const $stockSelect = $('#stock-select'); |
||||||
|
const $stockList = $('#stock-list'); |
||||||
|
const $submitBtn = $('#submit-btn'); |
||||||
|
|
||||||
|
// Function to add a new stock item to the list |
||||||
|
function addStockItem(stock) { |
||||||
|
// Add item to array |
||||||
|
stockList.push(stock); |
||||||
|
|
||||||
|
// Update HTML list |
||||||
|
const $newItem = $(`<li class="list-group-item">${stock}<button class="btn btn-sm btn-danger float-right delete-btn">X</button></li>`); |
||||||
|
$stockList.append($newItem); |
||||||
|
} |
||||||
|
|
||||||
|
// Function to delete a stock item from the list |
||||||
|
function deleteStockItem(itemIndex) { |
||||||
|
// Remove item from array |
||||||
|
stockList.splice(itemIndex, 1); |
||||||
|
|
||||||
|
// Update HTML list |
||||||
|
$stockList.children().eq(itemIndex).remove(); |
||||||
|
} |
||||||
|
|
||||||
|
// Function to handle form submission |
||||||
|
$stockForm.submit(function(event) { |
||||||
|
event.preventDefault(); |
||||||
|
|
||||||
|
// Get selected stock from form |
||||||
|
const selectedStock = $stockSelect.val(); |
||||||
|
if (selectedStock != null) { |
||||||
|
// Add new item to list |
||||||
|
addStockItem(selectedStock); |
||||||
|
|
||||||
|
// Clear input field |
||||||
|
$stockSelect.val(''); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Event listener for delete button clicks |
||||||
|
$stockList.on('click', '.delete-btn', function(event) { |
||||||
|
// Get index of item to delete |
||||||
|
const $deleteBtn = $(event.target); |
||||||
|
const itemIndex = $deleteBtn.parent().index(); |
||||||
|
|
||||||
|
// Delete item from list |
||||||
|
deleteStockItem(itemIndex); |
||||||
|
}); |
||||||
|
|
||||||
|
// Event listener for submit button click |
||||||
|
$submitBtn.click(function(event) { |
||||||
|
// Send stock list to server |
||||||
|
console.log(event.target) |
||||||
|
console.log(stockList) |
||||||
|
$.ajax({ |
||||||
|
url: '/submit', |
||||||
|
method: 'POST', |
||||||
|
data: { stockList: JSON.stringify(stockList) }, |
||||||
|
success: function(response) { |
||||||
|
console.log('Stock list submitted successfully'); |
||||||
|
}, |
||||||
|
error: function(xhr) { |
||||||
|
console.log('Error submitting stock list: ' + xhr.responseText); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
|
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,126 @@ |
|||||||
|
import json |
||||||
|
import time |
||||||
|
import numpy as np |
||||||
|
import pandas as pd |
||||||
|
|
||||||
|
from scipy.optimize import minimize |
||||||
|
|
||||||
|
class MVO(object): |
||||||
|
def __init__(self, data, market, ratio, role='max-sharpe'): |
||||||
|
length, self.num = data.shape |
||||||
|
tsize = int(length*ratio) |
||||||
|
self.data_return = data.pct_change().dropna().to_numpy() |
||||||
|
self.market_return = market.pct_change().dropna().to_numpy() |
||||||
|
self.train[:tsize, :] |
||||||
|
self.test[tsize:, :] |
||||||
|
self.train_market = self.market_return[:tsize] |
||||||
|
self.test_market = self.market_return[tsize:] |
||||||
|
@staticmethod |
||||||
|
def portfolio_info(w, ret, market_ret, rf=0): |
||||||
|
# return and drawdown |
||||||
|
retPort = ret@w # T-dimensional array |
||||||
|
cum_ret = (retPort+1).cumprod() |
||||||
|
rolling_max=np.maximum.accumulate(cum_ret) |
||||||
|
mdd = np.max((rolling_max - cum_ret)/rolling_max) |
||||||
|
|
||||||
|
## Sharpe Ratio |
||||||
|
stdPort = np.std(retPort) |
||||||
|
vol = stdPort*15.87451 |
||||||
|
annual_ret = np.mean(retPort) * 252 |
||||||
|
annual_sr = (annual_ret-rf) / vol |
||||||
|
|
||||||
|
## alpha, beta |
||||||
|
cov = np.cov(retPort, market_ret) |
||||||
|
beta = cov[0, 1] / cov[1, 1] |
||||||
|
alpha = annual_ret - rf - beta*(np.mean(market_ret) * 252 - rf) |
||||||
|
R2 = cov[0, 1]**2/(cov[0, 0] * cov[1, 1]) |
||||||
|
|
||||||
|
## n-day 95% VaR |
||||||
|
var10 = -annual_ret*(10/252) + 1.645*vol*(10/252)**(1/2) |
||||||
|
d = dict(annual_ret = annual_ret, |
||||||
|
vol=vol, |
||||||
|
mdd=mdd, |
||||||
|
annual_sr=annual_sr, |
||||||
|
beta=beta, |
||||||
|
alpha=alpha, |
||||||
|
var10=var10, |
||||||
|
R2=R2) |
||||||
|
return {key: round(d[key], 2) for key in d} |
||||||
|
@staticmethod |
||||||
|
def sharpe_ratio(w, ret): |
||||||
|
cov = np.cov(ret.T) |
||||||
|
print(cov.shape, w.shape) |
||||||
|
retPort = ret@w # T-dimensional array |
||||||
|
stdPort = np.std(retPort) |
||||||
|
return np.mean(retPort)/stdPort |
||||||
|
@staticmethod |
||||||
|
def sharpe_grad(w, ret, cov): |
||||||
|
manual_ret = np.mean(ret, axis=0) |
||||||
|
# print(cov.shape, w.shape) |
||||||
|
retPort = ret@w # T-dimensional array |
||||||
|
stdPort = np.std(retPort) |
||||||
|
g1=manual_ret/stdPort |
||||||
|
g2=np.mean(retPort)*(-0.5)*stdPort**(-3)*(2*cov@w) |
||||||
|
return g1+g2 |
||||||
|
@staticmethod |
||||||
|
def sortino_ratio(w, ret): |
||||||
|
retPort = ret@w # T-dimensional array |
||||||
|
stdPort = np.std(np.maximum(-retPort, 0)) |
||||||
|
return np.mean(retPort)/stdPort |
||||||
|
@staticmethod |
||||||
|
def sortino_grad(w, ret, cov_sor): |
||||||
|
manual_ret = np.mean(ret, axis=0) |
||||||
|
# print(cov.shape, w.shape) |
||||||
|
retPort = ret@w # T-dimensional arrayss |
||||||
|
stdPort = np.std(retPort) |
||||||
|
g1=manual_ret/stdPort |
||||||
|
g2=np.mean(retPort)*(-0.5)*stdPort**(-3)*(2*cov_sor@w) |
||||||
|
return g1+g2 |
||||||
|
@staticmethod |
||||||
|
def volatility(w, ret): |
||||||
|
retPort = ret@w # T-dimensional array |
||||||
|
stdPort = np.std(retPort) |
||||||
|
return stdPort |
||||||
|
@staticmethod |
||||||
|
def volatility_grad(w, ret, cov): |
||||||
|
retPort = ret@w # T-dimensional array |
||||||
|
stdPort = np.std(retPort) |
||||||
|
return cov@w/stdPort**(0.5) |
||||||
|
@classmethod |
||||||
|
def opt(cls, ret, role="max_sharpe"): |
||||||
|
n = ret.shape[1] |
||||||
|
init=np.ones(n)/n |
||||||
|
if role=="max_sharpe": |
||||||
|
cov=np.cov(ret.T) |
||||||
|
loss = lambda w: -cls.sharpe_ratio(w, ret) |
||||||
|
grad = lambda w: -cls.sharpe_grad(w, ret, cov) |
||||||
|
elif role=="max_sortino": |
||||||
|
cov = np.cov(np.maximum(ret, 0).T) |
||||||
|
loss = lambda w: -cls.sortino_ratio(w, ret) |
||||||
|
grad = lambda w: -cls.sortino_grad(w, ret, cov) |
||||||
|
elif role=="min_volatility": |
||||||
|
cov=np.cov(ret.T) |
||||||
|
loss = lambda w: cls.volatility(w, ret) |
||||||
|
grad = lambda w: cls.volatility_grad(w, ret, cov) |
||||||
|
else: |
||||||
|
return init |
||||||
|
bnds = [[0, 0.6] for i in range(n)] |
||||||
|
opts = {'maxiter': 10000, 'disp': False} |
||||||
|
cons = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1}) |
||||||
|
result = minimize(loss, init, method="SLSQP",\ |
||||||
|
options=opts, bounds=bnds, tol = None, jac = grad, constraints=cons) |
||||||
|
sol = result['x'] |
||||||
|
return np.round(sol, 2) |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@ |
|||||||
|
insert_strategy = \ |
||||||
|
""" |
||||||
|
insert into strategy (date, name, username, competition, role, ratio, annual_ret, vol, mdd, annual_sr, beta, alpha, var10, R2, tw, comment, assets, assets_position) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s); |
||||||
|
""" |
||||||
|
# (date, name, username, competition, role, ratio, annual_ret, vol, mdd, annual_sr, beta, alpha, var10, R2, tw, comment, assets, assets_position) |
@ -0,0 +1,44 @@ |
|||||||
|
DROP TABLE IF EXISTS stock_price; |
||||||
|
-- DROP TABLE IF EXISTS stock_price_tw; |
||||||
|
-- DROP TABLE IF EXISTS stock_info; |
||||||
|
-- DROP TABLE IF EXISTS stock_info_tw; |
||||||
|
|
||||||
|
CREATE TABLE stock_price ( |
||||||
|
id SERIAL PRIMARY KEY, |
||||||
|
ticker VARCHAR(64) NOT NULL, |
||||||
|
date DATE NOT NULL, |
||||||
|
price REAL NOT NULL, |
||||||
|
return REAL NOT NULL |
||||||
|
); |
||||||
|
-- you need to add () |
||||||
|
CREATE INDEX idx_ticker ON stock_price (ticker); |
||||||
|
|
||||||
|
-- CREATE TABLE stock_price_tw ( |
||||||
|
-- id SERIAL PRIMARY KEY, |
||||||
|
-- ticker VARCHAR(64) NOT NULL, |
||||||
|
-- date DATE NOT NULL, |
||||||
|
-- price REAL NOT NULL, |
||||||
|
-- return REAL NOT NULL |
||||||
|
-- ); |
||||||
|
-- -- you need to add () |
||||||
|
-- CREATE INDEX idx_ticker_tw ON stock_price_tw (ticker); |
||||||
|
|
||||||
|
-- CREATE TABLE stock_info_tw ( |
||||||
|
-- id SERIAL PRIMARY KEY, |
||||||
|
-- ticker VARCHAR(64) NOT NULL, |
||||||
|
-- last_price REAL NOT NULL, |
||||||
|
-- start DATE NOT NULL, |
||||||
|
-- end DATE NOT NULL |
||||||
|
-- ); |
||||||
|
-- you need to add () |
||||||
|
-- CREATE INDEX idx_info_tw ON stock_info_tw (ticker); |
||||||
|
|
||||||
|
-- CREATE TABLE stock_info ( |
||||||
|
-- id SERIAL PRIMARY KEY, |
||||||
|
-- ticker VARCHAR(64) NOT NULL, |
||||||
|
-- last_price REAL NOT NULL, |
||||||
|
-- start DATE NOT NULL, |
||||||
|
-- end DATE NOT NULL |
||||||
|
-- ); |
||||||
|
-- you need to add () |
||||||
|
-- CREATE INDEX idx_info ON stock_info (ticker); |
@ -0,0 +1,24 @@ |
|||||||
|
DROP TABLE IF EXISTS strategy; |
||||||
|
CREATE TABLE strategy ( |
||||||
|
id SERIAL PRIMARY KEY, |
||||||
|
date DATE NOT NULL, |
||||||
|
name VARCHAR(64) NOT NULL, |
||||||
|
username VARCHAR(64) NOT NULL, |
||||||
|
competition VARCHAR(64) NOT NULL, |
||||||
|
role VARCHAR(20) NOT NULL, |
||||||
|
ratio REAL NOT NULL, |
||||||
|
annual_ret REAL NOT NULL, |
||||||
|
vol REAL NOT NULL, |
||||||
|
mdd REAL NOT NULL, |
||||||
|
annual_sr REAL NOT NULL, |
||||||
|
beta REAL NOT NULL, |
||||||
|
alpha REAL NOT NULL, |
||||||
|
var10 REAL NOT NULL, |
||||||
|
R2 REAL NOT NULL, |
||||||
|
tw BOOLEAN DEFAULT TRUE, |
||||||
|
comment VARCHAR(255), |
||||||
|
assets TEXT[] NOT NULL, |
||||||
|
assets_position REAL[] NOT NULL |
||||||
|
); |
||||||
|
CREATE INDEX idx_user ON strategy (username); |
||||||
|
|
@ -0,0 +1,10 @@ |
|||||||
|
DROP TABLE IF EXISTS users; |
||||||
|
CREATE TABLE users ( |
||||||
|
user_id SERIAL PRIMARY KEY, |
||||||
|
username VARCHAR(64) UNIQUE NOT NULL, |
||||||
|
password VARCHAR(128) NOT NULL, |
||||||
|
vip BOOLEAN DEFAULT FALSE |
||||||
|
); |
||||||
|
INSERT INTO users (username, password) |
||||||
|
VALUES ('admin', 'pbkdf2:sha256:260000$Z5bK5pp0D8HEDAps$abb43b1b1c543ff334de8fb3aeba9c0460c37de5b5363e5210e68b00739d5e2c'), |
||||||
|
('R10246002@ntu.edu.tw', 'pbkdf2:sha256:260000$Z5bK5pp0D8HEDAps$abb43b1b1c543ff334de8fb3aeba9c0460c37de5b5363e5210e68b00739d5e2c'); |
After Width: | Height: | Size: 115 KiB |
After Width: | Height: | Size: 95 KiB |
After Width: | Height: | Size: 3.4 MiB |
After Width: | Height: | Size: 765 KiB |
@ -0,0 +1,156 @@ |
|||||||
|
// Initialize empty stock list
|
||||||
|
let stockList = ['2330.TW']; |
||||||
|
// Cache frequently-used DOM elements
|
||||||
|
const $stockForm = $('#stock-form'); |
||||||
|
const $stockSelect = $('#stock-select'); |
||||||
|
const $compSelect = $('#competition'); |
||||||
|
const $stockList = $('#stock-list'); |
||||||
|
const $submitBtn = $('#submit-btn'); |
||||||
|
const $addStockBtn = $('#addStockBtn'); |
||||||
|
const $submitPort = $('#submit-port'); |
||||||
|
const $sendPort = $('#sendPort'); |
||||||
|
const $commentPort = $('#commentPort'); |
||||||
|
|
||||||
|
// Function to add a new stock item to the list
|
||||||
|
function addStockItem(stock, text) { |
||||||
|
// Add item to array
|
||||||
|
stockList.push(stock); |
||||||
|
// Update HTML list
|
||||||
|
const $newItem = $(`<li class="list-group-item">
|
||||||
|
<span class="px-2">${text}</span> |
||||||
|
<a class="btn btn-sm btn-danger float-right delete-btn"> |
||||||
|
<i class="fas fa-trash-alt"></i> |
||||||
|
</a> |
||||||
|
</li>`); |
||||||
|
$stockList.append($newItem); |
||||||
|
} |
||||||
|
|
||||||
|
// Function to delete a stock item from the list
|
||||||
|
function deleteStockItem(itemIndex) { |
||||||
|
// Remove item from array
|
||||||
|
stockList.splice(itemIndex, 1); |
||||||
|
|
||||||
|
// Update HTML list
|
||||||
|
$stockList.children().eq(itemIndex).remove(); |
||||||
|
} |
||||||
|
|
||||||
|
// Event listener for delete button clicks
|
||||||
|
$stockList.on('click', '.delete-btn', function(){ |
||||||
|
var itemIndex = $(this).closest('li').index() |
||||||
|
deleteStockItem(itemIndex); |
||||||
|
// console.log(stockList);
|
||||||
|
}); |
||||||
|
// $stockList.on('click', '.delete-btn', function(event) {
|
||||||
|
// // Get index of item to delete
|
||||||
|
// const $deleteBtn = $(event.target);
|
||||||
|
// const itemIndex = $deleteBtn.parent().index();
|
||||||
|
|
||||||
|
// // Delete item from list
|
||||||
|
// deleteStockItem(itemIndex);
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
// Event listener for submit button click
|
||||||
|
$addStockBtn.click(function(event) { |
||||||
|
event.preventDefault(); |
||||||
|
$('#search').val(''); |
||||||
|
// Get selected stock from form
|
||||||
|
const selectedStock = $stockSelect.val(); |
||||||
|
var text = $('#stock-select option:selected').text(); |
||||||
|
if (selectedStock != null && stockList.indexOf(selectedStock)===-1) { |
||||||
|
// Add new item to list
|
||||||
|
addStockItem(selectedStock, text); |
||||||
|
|
||||||
|
// Clear input field
|
||||||
|
$stockSelect.val(''); |
||||||
|
} |
||||||
|
console.log(stockList); |
||||||
|
}); |
||||||
|
|
||||||
|
// Event listener for submit button click
|
||||||
|
$submitPort.click(function(event) { |
||||||
|
event.preventDefault(); |
||||||
|
if (stockList.length > 1){ |
||||||
|
$('#portModal').modal('show');
|
||||||
|
console.log('asset confirm'); |
||||||
|
// $(this).prop('disabled', true);
|
||||||
|
} |
||||||
|
}); |
||||||
|
// Event listener for submit button click
|
||||||
|
$sendPort.click(function(event) { |
||||||
|
if (stockList.length > 1){ |
||||||
|
$('#confirmMes').text("投資組合已開始建立,請等待完成訊息,或1分鐘後至分析結果區查看!") |
||||||
|
$('#confirmModal').modal('show');
|
||||||
|
|
||||||
|
|
||||||
|
$submitPort.prop('disabled', true); |
||||||
|
$.ajax({ |
||||||
|
url: '/postPort', //todo create_strategy
|
||||||
|
method: 'POST', |
||||||
|
data: {
|
||||||
|
name: $('input[name=portName]').val(), |
||||||
|
ts: Date.now(),
|
||||||
|
comp: $('#competition').val(), |
||||||
|
ratio: $('#ratio-select').val(), |
||||||
|
role: $('#role-select').val(), |
||||||
|
comment: $commentPort.val(), |
||||||
|
stockList: JSON.stringify(stockList) |
||||||
|
}, |
||||||
|
success: function(response) { |
||||||
|
console.log(response); |
||||||
|
// var res = JSON.parse(response);
|
||||||
|
event.preventDefault(); |
||||||
|
$('#confirmMes').text(response.mes) |
||||||
|
if (stockList.length > 0){ |
||||||
|
$('#confirmModal').modal('show');
|
||||||
|
} |
||||||
|
$submitPort.prop('disabled', false); |
||||||
|
}, |
||||||
|
error: function(xhr) { |
||||||
|
console.log('Error submitting stock list: ' + xhr.responseText); |
||||||
|
} |
||||||
|
}); |
||||||
|
$commentPort.val(''); |
||||||
|
} |
||||||
|
// Get selected stock from form
|
||||||
|
}); |
||||||
|
|
||||||
|
// Event listener for submit button click
|
||||||
|
$submitBtn.click(function(event) { |
||||||
|
// Send stock list to server
|
||||||
|
// console.log(event.target)
|
||||||
|
// console.log(stockList)
|
||||||
|
// console.log(cacheList.value, stockList);
|
||||||
|
if (stockList.length > 0) { |
||||||
|
// cacheList = stockList;
|
||||||
|
$.ajax({ |
||||||
|
url: '/postStock', //todo create_strategy
|
||||||
|
method: 'POST', |
||||||
|
data: { stockList: JSON.stringify(stockList) }, |
||||||
|
success: function(response) { |
||||||
|
var graphs = JSON.parse(response); |
||||||
|
Plotly.newPlot("graph", |
||||||
|
graphs.data, graphs.layout, {responsive: true}); |
||||||
|
console.log(response.layout); |
||||||
|
}, |
||||||
|
error: function(xhr) { |
||||||
|
console.log('Error submitting stock list: ' + xhr.responseText); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
$(document).ready(function(){ |
||||||
|
$("#search").on("keyup", function() { |
||||||
|
var value = $(this).val().toLowerCase(); |
||||||
|
$("#stock-select option").filter(function() { |
||||||
|
$(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
// window.onresize = function() {
|
||||||
|
// Plotly.relayout('graph', {
|
||||||
|
// 'xaxis.autorange': true,
|
||||||
|
// 'yaxis.autorange': true
|
||||||
|
// });
|
||||||
|
// };
|
@ -0,0 +1,284 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8" /> |
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||||
|
<title>{% block title %}NTHU Trading Platform{% endblock title%}</title> |
||||||
|
<link |
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" |
||||||
|
rel="stylesheet" |
||||||
|
integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" |
||||||
|
crossorigin="anonymous" |
||||||
|
/> |
||||||
|
<link |
||||||
|
rel="stylesheet" |
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css" |
||||||
|
/> |
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> |
||||||
|
{% block link %} |
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css"> |
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css"> |
||||||
|
{% endblock %} |
||||||
|
<style> |
||||||
|
body { |
||||||
|
padding-top: 70px; |
||||||
|
padding-bottom: 70px; |
||||||
|
background-color: #eee; |
||||||
|
/* font-size: 2vw; */ |
||||||
|
font-family: Georgia, sans-serif !important; |
||||||
|
} |
||||||
|
/* .offcanvas-body { |
||||||
|
font-size: 2vw |
||||||
|
} */ |
||||||
|
.navbar { |
||||||
|
background-image: linear-gradient(to bottom right, #5d9faa , #c4e0e5); |
||||||
|
} |
||||||
|
.navbar-nav > li > a.active { |
||||||
|
font-size: 18px; |
||||||
|
border-bottom: 2px ridge #888888; |
||||||
|
} |
||||||
|
p { |
||||||
|
text-indent: 2em; |
||||||
|
} |
||||||
|
{% block style %}{% endblock %} |
||||||
|
</style> |
||||||
|
|
||||||
|
</head> |
||||||
|
<header> |
||||||
|
|
||||||
|
{% set navigation_bar = [ |
||||||
|
('/', 'index', '首頁', 'bi bi-house-fill'), |
||||||
|
('/strategy', 'strategy', '建立策略', 'bi bi-piggy-bank-fill'), |
||||||
|
('/strategy_tw', 'strategy_tw', '台股建立策略', 'bi bi-piggy-bank-fill'), |
||||||
|
('/custom', 'custom', '自訂數據建立策略', 'bi bi-database-fill-add'), |
||||||
|
('/result', 'result', '分析結果排行', 'bi bi-graph-up-arrow'), |
||||||
|
('/result_tw', 'result_tw', '分析結果排行(台股)', 'bi bi-graph-up-arrow'), |
||||||
|
('mailto:r10246002@ntu.edu.tw', 'error', '錯誤回報', 'bi bi-bug-fill') |
||||||
|
] -%} |
||||||
|
{% set active_page = active_page|default('index') -%} |
||||||
|
<nav class="navbar bg-light fixed-top"> |
||||||
|
<div class="container-fluid px-3 py-2"> |
||||||
|
<a class="navbar-brand mx-2" href="{{ url_for('index') }}"> |
||||||
|
<h3><strong></i>投資組合大擂台</strong></h3> |
||||||
|
</a> |
||||||
|
<!-- TODO: Login/Logout --> |
||||||
|
<div class="navbar-expand ms-auto"> |
||||||
|
<ul class="navbar-nav me-2 mb-lg-0"> |
||||||
|
{% if not session.username %} |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link btn btn-lg" href="{{ url_for('login') }}"> |
||||||
|
<i class="bi bi-box-arrow-in-right"></i> |
||||||
|
<i class="bi bi-person-fill"></i> |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{% else %} |
||||||
|
<li class="nav-item dropdown"> |
||||||
|
<a class="nav-link dropdown-toggle btn btn-lg" href="" role="button" data-bs-toggle="dropdown" aria-expanded="true"> |
||||||
|
<i class="bi bi-person-check-fill"></i> |
||||||
|
</a> |
||||||
|
<ul class="dropdown-menu dropdown-menu-end" style="background-color: rgb(244, 250, 255);"> |
||||||
|
<li class="px-3 active">歡迎您: {{ session.username|safe }}</li> |
||||||
|
<li><hr class="dropdown-divider"></li> |
||||||
|
<li> |
||||||
|
<a class="dropdown-item" href="{{ url_for('logout') }}"> |
||||||
|
<i class="fas fa-sign-out-alt pe-2 "></i>登出 |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</li> |
||||||
|
{% endif %} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
<button |
||||||
|
class="navbar-toggler" |
||||||
|
type="button" |
||||||
|
data-bs-toggle="offcanvas" |
||||||
|
data-bs-target="#offcanvasNavbar" |
||||||
|
aria-controls="offcanvasNavbar"> |
||||||
|
<i class="bi bi-three-dots"></i> |
||||||
|
</button> |
||||||
|
<div |
||||||
|
class="offcanvas offcanvas-start text-bg-light {% if active_page is not none and not active_page == 'index'%}show{% endif %}" |
||||||
|
data-bs-scroll="true" |
||||||
|
tabindex="-1" |
||||||
|
id="offcanvasNavbar" |
||||||
|
aria-labelledby="offcanvasNavbarLabel" |
||||||
|
> |
||||||
|
<div class="offcanvas-header"> |
||||||
|
<h4 class="offcanvas-title mt-2 p-0" id="offcanvasNavbarLabel"> |
||||||
|
投資組合大擂台 |
||||||
|
</h4> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
class="btn-close btn-close" |
||||||
|
data-bs-dismiss="offcanvas" |
||||||
|
aria-label="Close" |
||||||
|
></button> |
||||||
|
</div> |
||||||
|
<div class="offcanvas-body"> |
||||||
|
<ul class="navbar-nav justify-content-end d-flex flex-grow-1 pe-3"> |
||||||
|
{% for href, id, caption, icon in navigation_bar %} |
||||||
|
<li class="nav-item my-2"> |
||||||
|
<a |
||||||
|
class="nav-link {% if id == active_page %}active{% endif %}" |
||||||
|
{% if id == active_page %}aria-current="page"{% endif %} |
||||||
|
href="{{ href|e }}"> |
||||||
|
<i class="{{ icon|e }}"></i> |
||||||
|
<span class="mx-1">{{ caption|e }}</span> |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{% endfor %} |
||||||
|
</ul> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</nav> |
||||||
|
</header> |
||||||
|
<body> |
||||||
|
{% block content %} |
||||||
|
<div class="container-sm mx-auto my-4 p-4 bg-white shadow-lg" style="border-radius: 5px;"> |
||||||
|
<!-- <h1 class="text-4xl font-bold mb-4">Document Title - {{ active_page|e }} </h1> --> |
||||||
|
<div class="alert alert-secondary" role="alert"> |
||||||
|
<ul> |
||||||
|
<li><i class="bi bi-caret-right-fill"></i> 本網站讓使用者可以自建投資組合,回測其績效,並與其他使用者比較、討論並改進。</li> |
||||||
|
<li><i class="bi bi-caret-right-fill"></i> 使用單位:清華大學、台灣大學、政治大學、明新科大、中華大學、臺北大學、證基會、成功大學。</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
<div class="prose lg:prose-xl p-3"> |
||||||
|
<h2 class="text-xl font-bold mb-4">平台簡介</h2> |
||||||
|
<p> |
||||||
|
我們常聽到的「鷄蛋不要放在同個籃子裏」, |
||||||
|
爲的就是要分散股市不確定性所帶來的風險。 |
||||||
|
一般來説,在風險分散的同時,收益也會跟著降低。 |
||||||
|
於是問題就變成:我們如何在風險與報酬率中取捨。 |
||||||
|
被稱爲資産配置之父的哈利·馬可維兹(Harry Max Markowitz)就提供了以下的想法。 |
||||||
|
</p> |
||||||
|
<p> |
||||||
|
在給定各個標的(underlying)的權重後, |
||||||
|
對該投資組合(portfolio)做均值–變異數分析(mean-variance analysis), |
||||||
|
其中平均數爲平均報酬率,變異數爲波動率(意即「風險」)。 |
||||||
|
在這樣定義不同投資組合的報酬與風險的情况下, |
||||||
|
我們可以發現:在不同的預期報酬下, |
||||||
|
都可以找到一個投資組合(或權重)使波動率達到最小值。 |
||||||
|
將這些報酬與對應到的最小風險記錄下來後所形成的開口向右的二維拋物線圖形(如下圖), |
||||||
|
就是所謂的「效率前緣(efficient frontier)」。 |
||||||
|
<div class='d-flex justify-content-center'> |
||||||
|
<div class="rounded mx-auto d-block" style="height: 80%; width: 80%;"> |
||||||
|
<img src="{{ url_for('static', filename='img/frontier.jpg') }}" class="img-fluid" alt="frontier-example"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
<p class="p-0"> |
||||||
|
本網站使用S&P 500、元大臺灣50以及Nasdaq 100指數的成份股的歷史日資料。 |
||||||
|
每次計算標的權重,都是取六個月的資料,依照馬可維兹的理論, |
||||||
|
畫出效率前緣,然後取夏普值最大的權重,再用接下來三個月的資料, |
||||||
|
觀察投資組合的價值如何變化。我們每三個月更新一次權重, |
||||||
|
再把許多三個月區間內投資組合的價值變動拼接起來, |
||||||
|
得到2015年中到2020年中投資組合的價值變動, |
||||||
|
再藉此求出平均年報酬、年波動率,再把報酬除以波動率得到整個投資期間的夏普值。 |
||||||
|
另外我們還算出最大跌幅(maximum drawdown),就是投資過程中, |
||||||
|
會從到目前為止的最高點,最多下跌多少幅度。 |
||||||
|
</p> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-6"> |
||||||
|
<div class="card mt-3"> |
||||||
|
<div class="card-header"> |
||||||
|
固定預期報酬 $p$,令投資組合權重為 $w$, 則將波動率最小化的數學問題為: |
||||||
|
</div> |
||||||
|
<div class="card-body"> |
||||||
|
<p class="card-text" style="font-size: 1.8vmin;"> |
||||||
|
$$\begin{equation} |
||||||
|
\begin{aligned} |
||||||
|
\min_{w} \quad &\frac{1}{2}w^{T}\Sigma w\quad\\ |
||||||
|
\textrm{s.t.} \quad &\sum_{i=1}^{n}w_i = 1\\ |
||||||
|
&\sum_{i=1}^{n}w_i R_i \geq p\\ |
||||||
|
&0\leq w_i \leq 1 \quad , 1 \leq i \leq n |
||||||
|
\end{aligned} |
||||||
|
\end{equation}$$ |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="col-sm-6"> |
||||||
|
<div class="card mt-3"> |
||||||
|
<div class="card-header"> |
||||||
|
令投資組合權重為 $w$, 則將夏普率最大化的數學問題為: |
||||||
|
</div> |
||||||
|
<div class="card-body"> |
||||||
|
<p class="card-text" style="font-size: 1.8vmin;"> |
||||||
|
$$\begin{equation} |
||||||
|
\begin{aligned} |
||||||
|
\min_{w} \quad &\frac{w^T R}{\sqrt{w^{T}\Sigma w}}\quad\\ |
||||||
|
\textrm{s.t.} \quad &\sum_{i=1}^{n}w_i = 1\\ |
||||||
|
&0\leq w_i \leq 1 \quad , 1 \leq i \leq n |
||||||
|
|
||||||
|
\end{aligned} |
||||||
|
\end{equation}$$ |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr class="my-4"> |
||||||
|
<h2 class="text-xl font-bold mb-4 mt-4">投資組合的$\alpha$、$\beta$值簡介</h2> |
||||||
|
<p class="my-1"> |
||||||
|
在評估投資組合的表現時,經常使用的是絕對性的指標, |
||||||
|
例如報酬率、波動率、夏普指數,MDD等, |
||||||
|
不過也有相對性的指標alpha、beta值。 |
||||||
|
比較投組與另一個標的(例如大盤指數, |
||||||
|
我們可以透過回歸式,估計出作為超額報酬的alpha值, |
||||||
|
以及作為相關性的beta值。Alpha值表示投組的獲利能力, |
||||||
|
越高越好。Beta值代表了投組對系統性風險的曝險程度,越低越好。 |
||||||
|
</p> |
||||||
|
<p class="my-1"> |
||||||
|
根據CAPM模型,給定投組報酬 $r_{p}$, 大盤指數 $r_m$ 以及無風險利率 $r_f$ , |
||||||
|
$\beta$ 值可以藉由以下公式得出: |
||||||
|
$$ \mathbb{E}[r_p] - r_f = \beta (\mathbb{E}[r_m] - r_f)$$ |
||||||
|
$\alpha$ 值則是藉由計算實際的投組報酬與由CAPM得到的理論報酬的差得到,公式為: |
||||||
|
$$\alpha = r_p - (r_f + \beta (r_m - r_f))$$ |
||||||
|
</p> |
||||||
|
<p class="my-1"> |
||||||
|
當給定一組時間序列時,alpha、beta值可以利用線性回歸的方式得出。 |
||||||
|
我們可以將上述的式子改寫成以下的形式: |
||||||
|
$$Y = \alpha + \beta X + \epsilon,$$ 而 $$Y = r_p-r_f , X = r_m-r_f$$ |
||||||
|
透過此回歸式,我們除了可以得到alpha、beta值外,也可以計算出值,用以衡量此模型的解釋力。 |
||||||
|
在本擂台上,不限台股的投組使用的大盤指數 ($r_m$) 是SPY, |
||||||
|
台股的投組使用的大盤指數是0050.TW,無風險利率暫定為0。 |
||||||
|
</p> |
||||||
|
<hr class="my-4"> |
||||||
|
<h2 class="text-xl font-bold mb-4 mt-4">VaR 簡介</h2> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endblock content %} |
||||||
|
|
||||||
|
|
||||||
|
<!--jQuery --> |
||||||
|
<script |
||||||
|
src="https://code.jquery.com/jquery-3.6.0.js" |
||||||
|
integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" |
||||||
|
crossorigin="anonymous" |
||||||
|
></script> |
||||||
|
<!-- Boostrap Scripts --> |
||||||
|
<script |
||||||
|
defer |
||||||
|
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" |
||||||
|
integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" |
||||||
|
crossorigin="anonymous" |
||||||
|
></script> |
||||||
|
{% block script %} |
||||||
|
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> |
||||||
|
<script id="MathJax-script" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.0/es5/tex-mml-chtml.min.js"></script> |
||||||
|
<script> |
||||||
|
MathJax = { |
||||||
|
tex: { |
||||||
|
inlineMath: [['$', '$'], ['\\(', '\\)']] |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,53 @@ |
|||||||
|
<option value="none" selected="selected">無</option> |
||||||
|
<option value="com_fin_2023">計算金融 (Spring, 2023)</option> |
||||||
|
<option value="MEPM_2022">總體經濟分析與投資組合管理(Dec. 2022)</option> |
||||||
|
<option value="Fin_2022_MUST">Fintech(MUST)</option> |
||||||
|
<option value="Fin_2022_Taitung">Fintech(台東大學)</option> |
||||||
|
<option value="Fin_2022_AI">金融科技能力建構_AI</option> |
||||||
|
<option value="Fin_NTHU_2022_Fall">Fintech(勞動部_NTHU, Oct. 2022-Jan. 2023)</option> |
||||||
|
<option value="LA_2022_Fall">LA (Fall 2022)</option> |
||||||
|
<option value="Fin_Engineer_2022_Fall">財工專題(Fall 2022)</option> |
||||||
|
<option value="Asia_Fin_2022_Fall">亞洲區域金融及金融創新與交易 (Fall 2022)</option> |
||||||
|
<option value="NCCU_2022_Fall">總體經濟分析與投資組合管理</option> |
||||||
|
<option value="NTU_2022_Fall">Fintech(NTU_CS,Fall,2022)</option> |
||||||
|
<option value="NTHU_2022_Fall">Fintech(NTHU_EE,Fall,2022)</option> |
||||||
|
|
||||||
|
<option value="Asia_fin_2022_Spring">亞洲區域金融及金融創新與交易(Spring, 2022)</option> |
||||||
|
<option value="NKUST_2022_Spring">高雄科技大學(Spring, 2022)</option> |
||||||
|
|
||||||
|
<option value="Fintech_2022_Spring_FCU">五校Fintech-逢甲(Spring, 2022)</option> |
||||||
|
<option value="Fintech_2022_Spring_TKU">五校Fintech-淡江(Spring, 2022)</option> |
||||||
|
<option value="Fintech_2022_Spring_FJU">五校Fintech-輔大(Spring, 2022)</option> |
||||||
|
<option value="Fintech_2022_Spring_NCCU">五校Fintech-政大(Spring, 2022)</option> |
||||||
|
<option value="Fintech_2022_Spring_NTHU">五校Fintech-清大(Spring, 2022)</option> |
||||||
|
<option value="BlockChain_2022_Spring">Blockchain (NTHU_QF, Spring 2022)</option> |
||||||
|
<option value="University_Students_Fin_2022_Spring">大專生金融專班 (Spring 2022)</option> |
||||||
|
<option value="Asia_Fin_2021_Fall">亞洲區域金融及金融創新與交易 (Fall 2021)</option> |
||||||
|
<option value="Labor_2021_Fall">Fintech (勞動部_NTHU, Nov. 2021-Jan. 2022)</option> |
||||||
|
<option value="Finance_engineer_2021_Fall">財工專題(Fall, 2021)</option> |
||||||
|
<option value="Asset_2021_Fall">資產管理AI應用實務(Fall, 2021)</option> |
||||||
|
<option value="NTU__2021_Fall">Fintech(NTU_CS, Fall, 2021)</option> |
||||||
|
<option value="NTHU_2021_Fall">Fintech(NTHU_EE, Fall, 2021)</option> |
||||||
|
<option value="NTHU-AIMS_2021_Fall">大數據技術實務應用(NTHU_AIMS, Fall 2021)</option> |
||||||
|
|
||||||
|
<option value="MaFin_NTU_Spring_2021">MaFin(NTU, Spring, 2021)</option> |
||||||
|
|
||||||
|
<option value="Service_NTHU_Spring_2023">服務學習(NTHU, Spring, 2021)</option> |
||||||
|
<option value="Asia_fin_Spring_2021">亞洲區域金融及金融創新與交易(Spring, 2021)</option> |
||||||
|
|
||||||
|
<option value="Fintech_AIMS_NTHU_2021_Spring">Fintech-AIMS NTHU, 2021 Spring</option> |
||||||
|
<!--option value="Fintech_AIMS_NTHU__2021_Spring">Fintech-AIMS NTHU, 2021 Spring</option--> |
||||||
|
<option value="NTHU-AIMS_2021_April">大數據技術實務應用 (NTHU-AIMS 2021 April)</option> |
||||||
|
|
||||||
|
<option value="MUST_Investment_Practice">MUST 投資實務</option> |
||||||
|
<option value="NTU__2021_Spring">Fintech (NTU, 2021 Spring)</option> |
||||||
|
<option value="NTHU_2021_Spring">Fintech (NTHU 2021 Spring)</option> |
||||||
|
<option value="NCCU_2021_Spring">Fintech (NCCU 2021 Spring)</option> |
||||||
|
<option value="MOL_NTHU_2021_March-June">Fintech (勞動部_NTHU 2021 March-June)</option> |
||||||
|
|
||||||
|
<option value="Fin_elite_class_CPMA">企經會理財菁英班</option> |
||||||
|
<option value="FE2020fall_Dec">財工專題(109年度上學期)12月競賽</option> |
||||||
|
<option value="FE2020fall_Jan">財工專題(109年度上學期)1月競賽</option> |
||||||
|
<option value="MUST_Fin">MUST財金</option> |
||||||
|
<option value="NTPU20201226">金融公益專班 臺北大學1226</option> |
||||||
|
<option value="ErikTest">Erik 測試競賽</option> |
@ -0,0 +1,6 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% set active_page = 'custom' %} |
||||||
|
|
||||||
|
|
||||||
|
{% block title %}Strategy Page{% endblock%} |
||||||
|
{% block content %}Hello world - {{ active_page|e }} {% endblock %} |
@ -0,0 +1,85 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% set active_page = none %} |
||||||
|
|
||||||
|
|
||||||
|
{% block title %}Login Page{% endblock %} |
||||||
|
{% block style %} |
||||||
|
.card-body { |
||||||
|
background-image: linear-gradient(rgba(255,255,255,0.5), rgba(255,255,255,0.5)), url({{ url_for('static', filename='img/cat.png') }}); |
||||||
|
background-attachment: fixed-bottom; |
||||||
|
background-position: right bottom -10px; |
||||||
|
background-repeat: no-repeat; |
||||||
|
<!-- background-size: 40%; --> |
||||||
|
} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<div class="container-fluid fade-in" style="background-color: #eee;"> |
||||||
|
<div class="container-fluid px-1 py-4"> |
||||||
|
<div class="row d-flex justify-content-center align-items-center h-100"> |
||||||
|
<div class="col-lg-12 col-xl-11"> |
||||||
|
<div class="card text-black" style="border-radius: 25px;"> |
||||||
|
<div class="card-body p-md-5" style="border-radius: 25px;"> |
||||||
|
<div class="row justify-content-center"> |
||||||
|
<div class="col-md-10 col-lg-6 col-xl-5 order-1 order-lg-1"> |
||||||
|
<p class="text-center h1 fw-bold mb-5 mx-1 mx-md-2 mt-4">登入</p> |
||||||
|
<form method="post"> |
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %} |
||||||
|
{% if messages %} |
||||||
|
{% for category, message in messages %} |
||||||
|
<div class="alert alert-{{ category }} alert-dismissible fade show mx-3 mb-3 mb-lg-3" role="alert"> |
||||||
|
{{ message }} |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
{% endfor %} |
||||||
|
{% endif %} |
||||||
|
{% endwith %} |
||||||
|
<div class="mb-3 p-3"> |
||||||
|
<label for="InputEmail1" class="form-label">Email address</label> |
||||||
|
<input type="email" class="form-control" id="InputEmail1" aria-describedby="emailHelp" name="username" required> |
||||||
|
<div id="emailHelp" class="form-text">We'll never share your email with anyone else.</div> |
||||||
|
</div> |
||||||
|
<div class="mb-3 p-3"> |
||||||
|
<label for="InputPassword1" class="form-label">Password</label> |
||||||
|
<input type="password" class="form-control" id="InputPassword1" name="password" required> |
||||||
|
</div> |
||||||
|
<div class="m-3 form-check"> |
||||||
|
<input type="checkbox" class="form-check-input" id="exampleCheck1"> |
||||||
|
<label class="form-check-label" for="exampleCheck1">I am human.</label> |
||||||
|
</div> |
||||||
|
<div class="d-flex justify-content-center mx-3 mb-4 mb-lg-4"> |
||||||
|
<div class="pt-2 flex-grow-1"> |
||||||
|
<a class="" href="{{ url_for('registration') }}" style="color:rgb(0, 81, 255);">沒有帳號?</a> |
||||||
|
</div> |
||||||
|
<button type="submit" class="btn btn-primary">送出</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
<!-- <div class="col-md-10 col-lg-6 col-xl-7 d-flex align-items-center order-1 order-lg-2"> |
||||||
|
<img src="{{ url_for('static', filename='img/cat.png') }}" |
||||||
|
class="img-fluid" alt="SINGUP IMAGE"> |
||||||
|
</div> --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- {% with messages = get_flashed_messages() %} |
||||||
|
{% if messages %} |
||||||
|
{% for message in messages %} |
||||||
|
<div class="alert alert-danger" role="alert"> |
||||||
|
{{ message }} |
||||||
|
</div> |
||||||
|
{% endfor %} |
||||||
|
{% endif %} |
||||||
|
{% endwith %} --> |
||||||
|
|
||||||
|
{% if error %} |
||||||
|
<div class="alert alert-danger" role="alert"> |
||||||
|
{{ error }} |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
{% endblock %} |
@ -0,0 +1,73 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% set active_page = none %} |
||||||
|
|
||||||
|
|
||||||
|
{% block title %}Registration Page{% endblock %} |
||||||
|
{% block content %} |
||||||
|
<section class="container-fluid" style="background-color: #eee;"> |
||||||
|
<div class="container-fluid px-1 py-4"> |
||||||
|
<div class="row d-flex justify-content-center align-items-center h-100"> |
||||||
|
<div class="col-lg-12 col-xl-11"> |
||||||
|
<div class="card text-black" style="border-radius: 25px;"> |
||||||
|
<div class="card-body p-md-5"> |
||||||
|
<div class="row justify-content-center"> |
||||||
|
<div class="col-md-10 col-lg-6 col-xl-5 order-2 order-lg-1"> |
||||||
|
|
||||||
|
<p class="text-center h1 fw-bold mb-5 mx-1 mx-md-2 mt-4">註冊</p> |
||||||
|
<form method="post"> |
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %} |
||||||
|
{% if messages %} |
||||||
|
{% for category, message in messages %} |
||||||
|
<div class="alert alert-{{ category }} alert-dismissible fade show mx-3 mb-3 mb-lg-3" role="alert"> |
||||||
|
{{ message }} |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
{% endfor %} |
||||||
|
{% endif %} |
||||||
|
{% endwith %} |
||||||
|
<div class="mb-3 px-3"> |
||||||
|
<label for="InputEmail1" class="form-label">Email address</label> |
||||||
|
<input type="email" class="form-control" id="InputEmail1" aria-describedby="emailHelp" name="username" required> |
||||||
|
<div id="emailHelp" class="form-text">We'll never share your email with anyone else.</div> |
||||||
|
</div> |
||||||
|
<div class="mb-3 px-3"> |
||||||
|
<label for="InputPassword1" class="form-label">Password</label> |
||||||
|
<input type="password" class="form-control" id="InputPassword1" name="password" required> |
||||||
|
</div> |
||||||
|
<div class="mb-3 px-3"> |
||||||
|
<label for="InputPassword2" class="form-label">Reapeat Yout Password</label> |
||||||
|
<input type="password" class="form-control" id="InputPassword2" name="rep-password" required> |
||||||
|
</div> |
||||||
|
<div class="d-flex justify-content-center mx-3 mb-4 mb-lg-4"> |
||||||
|
<div class="pt-2 flex-grow-1"> |
||||||
|
<a class="" href="{{ url_for('login') }}" style="color:rgb(0, 81, 255);">已有帳號?</a> |
||||||
|
</div> |
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">送出</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
<!-- <hr class="mx-3"> --> |
||||||
|
|
||||||
|
</div> |
||||||
|
<div class="col-md-10 col-lg-6 col-xl-7 d-flex align-items-center order-1 order-lg-2"> |
||||||
|
|
||||||
|
<img src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-registration/draw1.webp" |
||||||
|
class="img-fluid" alt="SINGUP IMAGE"> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
|
||||||
|
<!-- {% if error %} |
||||||
|
<div class="alert alert-danger" role="alert"> |
||||||
|
{{ error }} |
||||||
|
</div> |
||||||
|
{% endif %} --> |
||||||
|
</div> |
||||||
|
{% endblock %} |
@ -0,0 +1,6 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% set active_page = 'result' %} |
||||||
|
|
||||||
|
|
||||||
|
{% block title %}Strategy Page{% endblock%} |
||||||
|
{% block content %}Hello world - {{ active_page|e }} {% endblock %} |
@ -0,0 +1,8 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% set active_page = 'result_tw' %} |
||||||
|
|
||||||
|
|
||||||
|
{% block title %}Strategy Page{% endblock%} |
||||||
|
{% block content %}Hello world - {{ active_page|e }} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,175 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% if session.tw == 1 %} |
||||||
|
{% set active_page = 'strategy_tw' %} |
||||||
|
{% else %} |
||||||
|
{% set active_page = 'strategy' %} |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}Strategy Page{% endblock%} |
||||||
|
{% block style %} |
||||||
|
body { |
||||||
|
background-image: url({{ url_for('static', filename='img/money.jpeg') }}); |
||||||
|
background-repeat: no-repeat; /* Do not repeat the image */ |
||||||
|
background-size: cover; |
||||||
|
} |
||||||
|
.input-group{ |
||||||
|
border: 1px solid #8E8E8E; |
||||||
|
border-radius: 7px; |
||||||
|
} |
||||||
|
.input-group > span{ |
||||||
|
bg-dark; |
||||||
|
font-bold; |
||||||
|
} |
||||||
|
{% endblock style %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<div class="container-md"> |
||||||
|
<div class="container-md m-1"> |
||||||
|
<div class="card mb-3 p-4"> |
||||||
|
<div class="card mb-3"> |
||||||
|
<img src="{{ url_for("static", filename="img/stock.jpeg") }}" class="card-img-top" alt="..."> |
||||||
|
<div class="card-body"> |
||||||
|
<h5 class="card-title text-xl font-bold">投資組合建立步驟</h5> |
||||||
|
<ol class="list-group list-group-flush list-group-numbered"> |
||||||
|
<li class="list-group-item"><span class="ps-2">輸入投資組合名稱</span></li> |
||||||
|
<li class="list-group-item"><span class="ps-2">選擇所參加的課程或競賽</span></li> |
||||||
|
<li class="list-group-item"><span class="ps-2">選擇資產 (可使用搜索功能篩選)</span></li> |
||||||
|
<li class="list-group-item"><span class="ps-2">確認資產後按下 "確認資產", 查看資產價格動態圖表</span></li> |
||||||
|
<li class="list-group-item"><span class="ps-2">選擇訓練 / 回測數據比例與最佳化目標函數</span></li> |
||||||
|
<li class="list-group-item"><span class="ps-2">確認後按下 "建立投資組合", 查看回傳訊息</span></li> |
||||||
|
</ol> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="input-group mb-1 flex-nowrap"> |
||||||
|
<span class="input-group-text" id="addon-wrapping">投資組合名稱</span> |
||||||
|
<input name="portName" type="text" class="form-control" placeholder="EX. 空軍一號" aria-label="" aria-describedby="addon-wrapping" required> |
||||||
|
</div> |
||||||
|
<div class="input-group mb-3"> |
||||||
|
<span class="input-group-text" id="inputGroup-sizing-default">請選擇所參加的課程/競賽</span> |
||||||
|
<select id="competition" class="form-select" size="1" aria-label="size 5 select example"> |
||||||
|
{% include 'competitions.html' %} |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
<div class="input-group mb-1" > |
||||||
|
<span class="input-group-text" id="search1">搜尋</span> |
||||||
|
<input id="search" type="text" class="form-control" placeholder="輸入資產代號" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-default"> |
||||||
|
<span class="input-group-text" id="inputGroup-sizing-default">下方加入資產</span> |
||||||
|
</div> |
||||||
|
<div class="input-group mb-3"> |
||||||
|
<span class="input-group-text" id="inputGroup-sizing-default">請選擇資產</span> |
||||||
|
<select id="stock-select" class="form-select" size="2" aria-label="size 5 select example"> |
||||||
|
{% for key, data in stockOpts.items() -%} |
||||||
|
<option value="{{ key|e }}">{{ key|e }} | {{ data[0]|e }} | {{ data[2]|e }} ~</option> |
||||||
|
{% endfor %} |
||||||
|
</select> |
||||||
|
|
||||||
|
<button class="btn btn-secondary" |
||||||
|
type="button" |
||||||
|
id="addStockBtn"> |
||||||
|
加入 |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="card" > |
||||||
|
<div class="card-header d-flex"> |
||||||
|
<div class="py-2 font-bold text-xl"> |
||||||
|
已選擇的資產 |
||||||
|
</div> |
||||||
|
<div class="btn-group ms-auto"> |
||||||
|
<button type="button" class="btn btn-outline-primary " id="submit-btn"> |
||||||
|
確認資產 |
||||||
|
</button> |
||||||
|
<button type="button" class="btn btn-outline-danger" id="submit-port"> |
||||||
|
確認建立 |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="card-body"> |
||||||
|
<div class="input-group mb-3"> |
||||||
|
<span class="input-group-text me-auto bg-info" id="inputGroup3">請選擇訓練/回測數據比例</span> |
||||||
|
<select id="ratio-select" class="form-select" size="1" aria-label=""> |
||||||
|
<option value="0.5">5:5</option> |
||||||
|
<option value="0.6">6:4</option> |
||||||
|
<option selected value="0.7">7:3</option> |
||||||
|
<option value="0.8">8:2</option> |
||||||
|
<option value="0.9">9:1</option> |
||||||
|
</select> |
||||||
|
<select id="role-select" class="form-select" size="1" aria-label=""> |
||||||
|
<option selected value="max_sharpe">最大化夏普比率</option> |
||||||
|
<option value="max_sortino">最大化索提諾比率</option> |
||||||
|
<option value="min_volatility">最小化波動率</option> |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
<ol class="list-group list-group-numbered" id="stock-list" type="1"> |
||||||
|
<li class="list-group-item"> |
||||||
|
<span class="px-2">2330.TW | 台積電 | 2000-01-05 ~</span> |
||||||
|
<a class="btn btn-sm btn-danger float-right delete-btn"> |
||||||
|
<i class="fas fa-trash-alt"></i> |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
</ol> |
||||||
|
</div> |
||||||
|
<hr class="my-3"> |
||||||
|
<div id="graph" style="border-radius: 10px;"></div> |
||||||
|
</div> |
||||||
|
<div class="modal" id="portModal" tabindex="-1"> |
||||||
|
<div class="modal-dialog"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title font-bold text-xl">確認建立投資組合</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<span> |
||||||
|
確認後將會暫時關閉建立按鈕,請等待完成訊息! |
||||||
|
</span> |
||||||
|
<ol class="list-group list-group-flush list-group-numbered"> |
||||||
|
<li class="list-group-item"><span class="ps-2">輸入投資組合名稱</span></li> |
||||||
|
<li class="list-group-item"><span class="ps-2">選擇所參加的課程或競賽</span></li> |
||||||
|
<li class="list-group-item"><span class="ps-2">選擇資產 (可使用搜索功能篩選)</span></li> |
||||||
|
<li class="list-group-item"><span class="ps-2">確認資產後按下 "確認資產", 查看資產價格動態圖表</span></li> |
||||||
|
<li class="list-group-item"><span class="ps-2">選擇訓練/測試集比率, 確認後按下 "建立投資組合", 查看回傳訊息</span></li> |
||||||
|
</ol> |
||||||
|
<div class="input-group"> |
||||||
|
<span class="input-group-text">輸入筆記</span> |
||||||
|
<textarea id="commentPort" class="form-control" aria-label="With textarea"></textarea> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> |
||||||
|
<button id="sendPort" type="button" class="btn btn-primary" data-bs-dismiss="modal">確認</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="modal" id="confirmModal" tabindex="-1"> |
||||||
|
<div class="modal-dialog"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title font-bold text-xl">開始建立投資組合</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<span id="confirmMes">投資組合已開始建立,請1分鐘後至結果分析查詢。</span> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">確認</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% block script %} |
||||||
|
<script src="{{ url_for('static', filename='js/addStock.js') }}"></script> |
||||||
|
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script> |
||||||
|
{% endblock script %} |
||||||
|
|
Loading…
Reference in new issue