@ -0,0 +1,9 @@ |
||||
FROM python:3.9.6 |
||||
WORKDIR /flask |
||||
ADD . /flask |
||||
RUN apt-get update |
||||
RUN apt install nano |
||||
RUN pip install --upgrade pip |
||||
RUN pip3 install -r requirements.txt |
||||
|
||||
EXPOSE 8000 |
||||
@ -0,0 +1 @@ |
||||
web: gunicorn main:app --preload --workers 28 --timeout 120 |
||||
@ -0,0 +1,46 @@ |
||||
import os |
||||
import time |
||||
from datetime import datetime, date, timedelta |
||||
# PARAMETERS |
||||
CONFIGS = { |
||||
# "ENV": "development", |
||||
# "DEBUG": True, |
||||
# "SQLALCHEMY_DATABASE_URI" : os.getenv('DATABASE_URL'), |
||||
"SECRET_KEY": os.urandom(30), # Set the secret key for session authentication |
||||
"PERMANENT_SESSION_LIFETIME": timedelta(minutes=60) |
||||
} |
||||
# SQL_CONFIG = dict( |
||||
# database= os.getenv("PGDATABASE"), |
||||
# user=os.getenv("PGUSER"), |
||||
# host=os.getenv("PGHOST"), |
||||
# port=os.getenv("PGPORT"), |
||||
# password=os.getenv("PGPASSWORD") |
||||
# ) |
||||
# CACHE_CONFIG = { |
||||
# 'CACHE_TYPE': 'redis', |
||||
# 'CACHE_REDIS_USER': os.getenv("REDISUSER"), |
||||
# 'CACHE_REDIS_HOST': os.getenv("REDISHOST"), |
||||
# 'CACHE_REDIS_PORT': os.getenv("REDISPORT"), |
||||
# 'CACHE_REDIS_PASSWORD': os.getenv("REDISPASSWORD"), |
||||
# 'CACHE_KEY_PREFIX': 'railway_redis_' |
||||
# } |
||||
|
||||
SQL_CONFIG = dict( |
||||
database="profolio_platform", |
||||
user="postgres", |
||||
host="db", |
||||
port="5432", |
||||
password="password" |
||||
) |
||||
CACHE_CONFIG = { |
||||
'CACHE_TYPE': 'redis', |
||||
# 'CACHE_REDIS_USER': 'default', |
||||
'CACHE_REDIS_HOST': 'redis', |
||||
'CACHE_REDIS_PORT': 6379, |
||||
# 'CACHE_REDIS_PASSWORD': '5rP99RevPMW94rswBXAL', |
||||
# 'CACHE_KEY_PREFIX': 'railway_redis_' |
||||
} |
||||
role_map = dict(max_sharpe='最大化夏普比率', |
||||
max_sortino='最大化索提諾比率', |
||||
min_volatility='最小化波動率', |
||||
quadratic_utility='最大化效用函數') |
||||
@ -0,0 +1,36 @@ |
||||
import psycopg2 |
||||
import yfinance as yf |
||||
import numpy as np |
||||
from psycopg2.extensions import register_adapter, AsIs |
||||
psycopg2.extensions.register_adapter(np.int64, psycopg2._psycopg.AsIs) |
||||
|
||||
ticker = 'SPY' |
||||
start = '2011-8-1' |
||||
end = '2021-9-1' |
||||
stock_data = yf.download(ticker, start=start, end=end ) |
||||
stock_data.index = np.datetime_as_string(stock_data.index, unit='D') |
||||
print(stock_data.index) |
||||
stock_data['Ticker'] = ticker |
||||
stock_data = stock_data.rename(columns={"Adj Close": "Adj_Close"}) |
||||
column = ["Open" , "High" , "Low" , "Close" , "Volume"] |
||||
stock_data = stock_data.drop(column, axis=1) |
||||
records = stock_data.to_records(index=True) |
||||
print(records) |
||||
|
||||
conn = psycopg2.connect( |
||||
database="profolio_platform", user='postgres', password='password', host='db',port ='5432' |
||||
) |
||||
conn.autocommit = True |
||||
cur = conn.cursor() |
||||
# print(stock_data["Date"]) |
||||
# print(stock_data["Adj_Close"]) |
||||
# print(stock_data["Ticker"]) |
||||
# print(type(stock_data["Date"])) |
||||
# print(type(stock_data["Adj_Close"])) |
||||
# print(type(stock_data["Ticker"])) |
||||
query = """INSERT INTO stock_price (date,price,ticker) |
||||
VALUES (%s, %s, %s)""" |
||||
cur = conn.cursor() |
||||
cur.executemany(query, records) |
||||
conn.close() |
||||
print("Data Insert Successfully") |
||||
@ -0,0 +1,60 @@ |
||||
version: "3.7" |
||||
services: |
||||
db: |
||||
build: |
||||
context: ./sql_script/ |
||||
image: postgres:15.3-alpine |
||||
environment: |
||||
POSTGRES_DB: porfolio_platform |
||||
POSTGRES_USER: postgres |
||||
POSTGRES_PASSWORD: password |
||||
PGDATA: /var/lib/postgresql/data |
||||
volumes: |
||||
- db-data:/var/lib/postgresql/data |
||||
ports: |
||||
- "5432:5432" |
||||
networks: |
||||
- common_network |
||||
pgadmin: |
||||
image: dpage/pgadmin4:latest |
||||
environment: |
||||
PGADMIN_DEFAULT_EMAIL: jjjoey020629@gmail.com |
||||
PGADMIN_DEFAULT_PASSWORD: password |
||||
PGADMIN_LISTEN_PORT: 90 |
||||
ports: |
||||
- "7070:90" |
||||
volumes: |
||||
- pgadmin-data:/var/lib/pgadmin |
||||
links: |
||||
- "db:pgsql-server" |
||||
networks: |
||||
- common_network |
||||
redis: |
||||
image: redis:7.0.11-alpine |
||||
container_name: myredis |
||||
ports: |
||||
- 6379:6379 |
||||
volumes: |
||||
- /data/redis-data:/data |
||||
networks: |
||||
- common_network |
||||
flask: |
||||
build: ./ |
||||
container_name: flask |
||||
command: python main.py runserver 0.0.0.0:8000 |
||||
depends_on: |
||||
- db |
||||
- pgadmin |
||||
- redis |
||||
ports: |
||||
- 8000:8000 |
||||
links: |
||||
- 'db' |
||||
- 'redis' |
||||
networks: |
||||
- common_network |
||||
volumes: |
||||
db-data: |
||||
pgadmin-data: |
||||
networks: |
||||
common_network: |
||||
@ -0,0 +1,537 @@ |
||||
#coding=utf-8 |
||||
from flask import Flask, render_template, request, redirect, url_for, g, session, flash, jsonify |
||||
# from flask_login import LoginManager, UserMixin, login_user, current_user, login_required, logout_user |
||||
from flask_sqlalchemy import SQLAlchemy |
||||
from flask_caching import Cache |
||||
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 logging |
||||
import numpy as np |
||||
import pandas as pd |
||||
import psycopg2 |
||||
import psycopg2.extras |
||||
import plotly |
||||
import plotly.express as px |
||||
from portfolio_builder import MVO |
||||
from config import * |
||||
pd.options.plotting.backend = "plotly" |
||||
|
||||
|
||||
|
||||
app = Flask(__name__) |
||||
app.config.from_mapping(CONFIGS) |
||||
app.config.update(CACHE_CONFIG) |
||||
cache = Cache(app) |
||||
|
||||
|
||||
|
||||
# Load Assets |
||||
with open('assets_tw.json') as f: |
||||
data_tw = json.load(f) |
||||
with open('assets_us.json') as f: |
||||
data_us = json.load(f) |
||||
|
||||
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 FROM stock_price_tw where ticker = ANY(%s);" |
||||
with conn: |
||||
with conn.cursor() as curs: |
||||
curs.execute(sql, (stock_list, )) |
||||
data= curs.fetchall() |
||||
else: |
||||
sql1="SELECT ticker, date, price FROM stock_price where ticker = ANY(%s)" |
||||
sql2="SELECT ticker, date, price 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']) |
||||
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 |
||||
def rolling_optimize(ret, lookback=126, backtest=126, role="max_sharpe", gamma=None): |
||||
n, num = ret.shape |
||||
period = (n - lookback)//backtest+1 |
||||
weights = [] |
||||
start = [] |
||||
rets = [] |
||||
for i in range(period): |
||||
curr = i*backtest+lookback |
||||
data_train = ret.iloc[curr-lookback:curr, :].to_numpy() |
||||
data_test = ret.iloc[curr:curr+backtest, :] |
||||
if len(data_test) == 0: |
||||
break |
||||
w = MVO.opt(data_train, role=role, gamma=gamma) |
||||
start.append(data_test.index[0]) |
||||
weights.append(w) |
||||
rets.append(data_test.to_numpy()@w) |
||||
weight = pd.DataFrame(weights, columns=ret.columns, index=pd.to_datetime(start)) |
||||
rets = np.hstack(rets) |
||||
equally_weighted = ret.iloc[lookback:, :].to_numpy()@np.ones(num)/num |
||||
rets = pd.DataFrame(np.vstack([rets, equally_weighted]).T, columns=['Portfolio', 'Equally'], index=ret.index[lookback:]) |
||||
return weight, rets |
||||
|
||||
# Define the route for the index pages |
||||
@app.route('/') |
||||
# @cache.cached(timeout=300) |
||||
def index(): |
||||
return render_template('base.html') |
||||
|
||||
# Login Page |
||||
@app.route('/login') |
||||
def login(): |
||||
if 'username' in session: |
||||
return render_template('base.html') |
||||
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.split('@')[0] |
||||
session['user_id'] = data[0] |
||||
session['privilege'] = data[-1] |
||||
session['update_freq'] = 100 |
||||
session['lastCreateTime'] = time.time() |
||||
session['tw'] = 1 |
||||
session['currStockList'] = [] |
||||
flash(f"成功登入,歡迎您 {session['username']} !", 'success') |
||||
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: |
||||
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: |
||||
session.clear() |
||||
flash(f"成功登出!", 'success') |
||||
return redirect(url_for('index')) |
||||
|
||||
|
||||
|
||||
@app.route('/strategy') |
||||
# @cache.cached(timeout=60) |
||||
def strategy(): |
||||
if login_required(): |
||||
pass |
||||
else: |
||||
flash('使用投組功能請先登入。', 'warning') |
||||
return redirect(url_for('login')) |
||||
session['tw'] = 0 |
||||
return render_template('strategy_tw.html', data_us = data_us, data_tw=data_tw, stock=['TSLA']) |
||||
|
||||
|
||||
|
||||
|
||||
@app.route('/strategy_tw') |
||||
# @cache.cached(timeout=60) |
||||
def strategy_tw(): |
||||
if login_required(): |
||||
pass |
||||
else: |
||||
flash('使用投組功能請先登入。', 'warning') |
||||
return redirect(url_for('login')) |
||||
session['tw'] = 1 |
||||
return render_template('strategy_tw.html', data_tw=data_tw, stock=['2330.TW']) |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@app.route('/postStock', methods=['POST']) |
||||
def submit_stock_list(): |
||||
if login_required(): |
||||
pass |
||||
else: |
||||
flash('使用投組功能請先登入。', 'warning') |
||||
return redirect(url_for('login')) |
||||
if not 'tw' in session: |
||||
return redirect(url_for('index')) |
||||
# Update Session |
||||
# app.logger.info("UPDATE ASSET") |
||||
if session['update_freq']==0: |
||||
# app.logger.info("Too Frequent") |
||||
return 'update to frquent!' |
||||
else: |
||||
session['update_freq']-=1 |
||||
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']) |
||||
if len(port.index) > 1008: |
||||
port = port.iloc[-1008:, :] |
||||
conn.close() |
||||
port = port.iloc[::3, :] |
||||
port = port/port.iloc[0, :] |
||||
|
||||
fig = port.plot(title='資產價格變化', labels=dict(index="Date", value="Price", variable="Assets")) |
||||
fig['layout'] = {} |
||||
# 序列化 |
||||
graphJSON = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) |
||||
# for key in request.form: |
||||
# app.logger.info("KEY: %s, ", key) |
||||
# print(key, request.form[key]) |
||||
# Do something with the stock list heres |
||||
return graphJSON |
||||
|
||||
@app.route('/postPort', methods=['POST']) |
||||
def buildPort(): |
||||
if login_required(): |
||||
pass |
||||
else: |
||||
flash('使用投組功能請先登入。', 'warning') |
||||
return redirect(url_for('login')) |
||||
if not 'tw' in session: |
||||
return redirect(url_for('index')) |
||||
# Stop frequently building strategy |
||||
if time.time() - session['lastCreateTime'] < 60: |
||||
less = round(time.time()-session['lastCreateTime'], 1) |
||||
print(f"UNTIL: {less}") |
||||
return f'<span>投資組合建立時間間隔(或與登入時間間隔)必須大於60秒!還差: {less} 秒。</span>' |
||||
session['lastCreateTime'] = time.time() |
||||
# for key in request.form: |
||||
# print(key, request.form[key], type(request.form[key])) |
||||
|
||||
# Portfolio Info , random name generator |
||||
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)}" |
||||
|
||||
# Opt Parameters |
||||
comp = request.form.get('comp') |
||||
ts = request.form.get('ts') |
||||
# ts = datetime.now().strftime("%Y-%m-%d, %H:%M:%S") |
||||
role = request.form.get('role') |
||||
lookback = int(request.form.get('lookback')) |
||||
backtest = int(request.form.get('frequency')) |
||||
gamma = float(request.form.get('gamma'))/100 |
||||
comment = request.form.get('comment') |
||||
stock_list = json.loads(request.form.get('stockList')) |
||||
|
||||
|
||||
# Algorithm MVO |
||||
print(f"{'-'*10}Enter Algorithms{'-'*10}") |
||||
# 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] |
||||
# Optimization |
||||
n = len(port.index) |
||||
if n < lookback+backtest+63: |
||||
return f'''<span>投資組合無法建立,資料長度與所選參數不符。</span>''' |
||||
elif n > 1009+lookback: |
||||
port = port.iloc[-(1009+lookback):, :] |
||||
market = market.iloc[-1009:] |
||||
else: |
||||
market = market.iloc[lookback:] |
||||
|
||||
length, num = port.shape |
||||
ret = port.pct_change().dropna() |
||||
weight, rets = rolling_optimize(ret, lookback, backtest, role=role, gamma=gamma) |
||||
weight.index = weight.index.astype(str) |
||||
|
||||
rets.index = rets.index.astype(str) |
||||
rets= rets.round(5) |
||||
|
||||
|
||||
# Get portfolio info. |
||||
info = MVO.portfolio_info(np.array([1]), rets['Portfolio'].to_numpy().reshape(-1, 1), market.pct_change().dropna().to_numpy()) |
||||
data = (ts, name, session.get('username').split('@')[0], comp, role, info['annual_ret'], |
||||
info['vol'], info['mdd'], info['annual_sr'], |
||||
info['beta'], info['alpha'], info['var10'], info['R2'], gamma, True, comment, stock_list, json.dumps(weight.to_dict(orient="split")), json.dumps(rets.to_dict(orient="split"))) |
||||
sql='insert into strategy \ |
||||
(date, name, username,\ |
||||
competition, role, annual_ret,\ |
||||
vol, mdd, annual_sr, beta, alpha,\ |
||||
var10, R2, gamma, tw, notes, assets, weight, ret)\ |
||||
values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING id;' |
||||
with conn: |
||||
with conn.cursor() as curs: |
||||
print("DATA : ",data) |
||||
curs.execute(sql, data) |
||||
|
||||
strategy_id = curs.fetchone()[0] |
||||
conn.close() |
||||
print(f"{'-'*10}Strategy write in Success{'-'*10}") |
||||
return f'''<span>投資組合已完成建立,請點擊 <a class="badge rounded-pill text-bg-info" href="/result_view?strategy_id={strategy_id}">{strategy_id}</a>查詢分析結果。</span>''' |
||||
|
||||
|
||||
|
||||
@app.route('/custom') |
||||
# @cache.cached(timeout=60) |
||||
def custom(): |
||||
if login_required(): |
||||
pass |
||||
else: |
||||
flash('使用投組功能請先登入。', 'warning') |
||||
return redirect(url_for('login')) |
||||
return render_template('custom.html') |
||||
@app.route('/custom', methods=['POST']) |
||||
def custom_post(): |
||||
if login_required(): |
||||
pass |
||||
else: |
||||
flash('使用投組功能請先登入。', 'warning') |
||||
return redirect(url_for('login')) |
||||
port = pd.read_csv(request.files['csv_file'], index_col=0, parse_dates=True) |
||||
role = request.form.get('role') |
||||
lookback = int(request.form.get('lookback')) |
||||
backtest = int(request.form.get('frequency')) |
||||
gamma = float(request.form.get('gamma'))/100 |
||||
# Optimization |
||||
n = len(port.index) |
||||
if n < lookback+backtest+63: |
||||
return f'''<span>投資組合無法建立,資料長度與所選參數不符。</span>''' |
||||
elif n > 1009+lookback: |
||||
port = port.iloc[-(1009+lookback):, :] |
||||
else: |
||||
pass |
||||
length, num = port.shape |
||||
ret = port.pct_change().dropna() |
||||
weight, rets = rolling_optimize(ret, lookback, backtest, role=role, gamma=gamma) |
||||
weight.index = weight.index.astype(str) |
||||
rets.index = rets.index.astype(str) |
||||
rets= rets.round(5) |
||||
|
||||
info = MVO.portfolio_info(np.array([1]), rets['Portfolio'].to_numpy().reshape(-1, 1), np.zeros(len(ret)-lookback)) |
||||
info['username'] = session.get('username').split('@')[0] |
||||
info['role'] = role_map[role] |
||||
info['id']='Custom data' |
||||
info['name']='Custom data' |
||||
info['date'] = '-' |
||||
info['alpha'] = '-' |
||||
info['beta'] = '-' |
||||
info['r2'] = '-' |
||||
info['assets'] = list(port.columns) |
||||
# Plotting weight |
||||
fig = px.bar(weight) |
||||
fig['layout'] = {} |
||||
info['weight'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) |
||||
# Plotting weight |
||||
fig = (rets+1).cumprod().iloc[::5, :].plot() |
||||
fig['layout'] = {} |
||||
info['ret'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) |
||||
# Plotting ret bars |
||||
rets.index.name = 'date' |
||||
rets.index = pd.to_datetime(rets.index) |
||||
ret_hist = rets.to_period('Q').groupby('date').apply(lambda x: (x+1).prod()-1) |
||||
ret_hist.index = ret_hist.index.astype(str) |
||||
fig = px.bar(ret_hist) |
||||
fig['layout'] = {} |
||||
info['bar'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) |
||||
return render_template('result_view.html', data=info) |
||||
|
||||
@cache.memoize(60) |
||||
def getStrategy(): |
||||
conn = psycopg2.connect(**SQL_CONFIG) |
||||
with conn: |
||||
with conn.cursor() as curs: |
||||
sql="select id, date, name, username, annual_ret, vol, annual_sr, mdd\ |
||||
from strategy order by id desc limit 50" |
||||
curs.execute(sql) |
||||
data= curs.fetchall() |
||||
conn.close() |
||||
return data |
||||
@cache.memoize(60) |
||||
def getPostStrategy(role, comp): |
||||
if role == "my": |
||||
conn = psycopg2.connect(**SQL_CONFIG) |
||||
with conn: |
||||
with conn.cursor() as curs: |
||||
sql=f"select id, date, name, username, annual_ret, vol, annual_sr, mdd\ |
||||
from strategy where username=%s order by id desc limit 50;" |
||||
curs.execute(sql, (session['username'], )) |
||||
data= curs.fetchall() |
||||
conn.close() |
||||
return data |
||||
if role in ['id', 'annual_ret', 'annual_sr', 'vol']: |
||||
pass |
||||
else: |
||||
role='id' |
||||
if role == 'vol': |
||||
order= 'asc' |
||||
else: |
||||
order= 'desc' |
||||
if comp == 'none': |
||||
comp=None |
||||
conn = psycopg2.connect(**SQL_CONFIG) |
||||
with conn: |
||||
with conn.cursor() as curs: |
||||
if comp is None: |
||||
sql=f"select id, date, name, username, annual_ret, vol, annual_sr, mdd\ |
||||
from strategy order by {escape(role)} {escape(order)} limit 50" |
||||
curs.execute(sql) |
||||
else: |
||||
sql=f"select id, date, name, username, annual_ret, vol, annual_sr, mdd\ |
||||
from strategy where competition=%s order by {escape(role)} {escape(order)} limit 50;" |
||||
curs.execute(sql, (comp, )) |
||||
data= curs.fetchall() |
||||
conn.close() |
||||
return data |
||||
@app.route('/result', methods=['GET', 'POST']) |
||||
def result(): |
||||
if login_required(): |
||||
pass |
||||
else: |
||||
flash('使用投組功能請先登入。', 'warning') |
||||
return redirect(url_for('login')) |
||||
if request.method=='GET': |
||||
data = getStrategy() |
||||
return render_template('result.html', strategy_data=data) |
||||
elif request.method=='POST': |
||||
role = request.form.get('role') |
||||
comp = request.form.get('competition') |
||||
data = getPostStrategy(role, comp) |
||||
return render_template('result.html', strategy_data=data) |
||||
|
||||
@app.route('/result_view') |
||||
def result_view(): |
||||
if login_required(): |
||||
pass |
||||
else: |
||||
flash('使用投組功能請先登入。', 'warning') |
||||
return redirect(url_for('login')) |
||||
if not 'strategy_id' in request.args: |
||||
return redirect(url_for('index')) |
||||
else: |
||||
sid = request.args.get('strategy_id') |
||||
strategy_id = request.args.get('strategy_id') |
||||
sql="""select * from strategy where id=%s;""" |
||||
conn = psycopg2.connect(**SQL_CONFIG) |
||||
with conn: |
||||
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as curs: |
||||
curs.execute(sql, (sid, )) |
||||
data= curs.fetchone() |
||||
conn.close() |
||||
# Processing data |
||||
data = dict(data) |
||||
data['role'] = role_map[data['role']] |
||||
w = data['weight'] |
||||
r = data['ret'] |
||||
w = pd.DataFrame(w['data'], columns=w['columns'], index=w['index']) |
||||
r = pd.DataFrame(r['data'], columns=r['columns'], index=r['index']) |
||||
# Plotting weight |
||||
fig = px.bar(w) |
||||
fig['layout'] = {} |
||||
data['weight'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) |
||||
# Plotting weight |
||||
fig = (r+1).cumprod().iloc[::5, :].plot() |
||||
fig['layout'] = {} |
||||
data['ret'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) |
||||
# Plotting ret bars |
||||
r.index.name = 'date' |
||||
r.index = pd.to_datetime(r.index) |
||||
ret_hist = r.to_period('Q').groupby('date').apply(lambda x: (x+1).prod()-1) |
||||
ret_hist.index = ret_hist.index.astype(str) |
||||
fig = px.bar(ret_hist) |
||||
fig['layout'] = {} |
||||
data['bar'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) |
||||
session['currStockList'] = data['assets'] |
||||
return render_template('result_view.html', data=data) |
||||
@app.route('/copy_portfolio') |
||||
def copy_portfolio(): |
||||
if login_required(): |
||||
pass |
||||
else: |
||||
flash('使用投組功能請先登入。', 'warning') |
||||
return redirect(url_for('login')) |
||||
if not 'tw' in session: |
||||
return redirect(url_for('index')) |
||||
session['tw'] = 0 |
||||
return render_template('strategy_tw.html', data_us = data_us, data_tw=data_tw, stock=session['currStockList']) |
||||
|
||||
@app.errorhandler(404) |
||||
def page_not_found(e): |
||||
# note that we set the 404 status explicitly |
||||
return render_template('404.html'), 404 |
||||
|
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
app.run(host='127.0.0.1', port=8000) |
||||
@ -0,0 +1,143 @@ |
||||
import json |
||||
import time |
||||
import numpy as np |
||||
import pandas as pd |
||||
from scipy.optimize import minimize |
||||
|
||||
class MVO(object): |
||||
@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)*stdPort**(-3)*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)*stdPort**(-3)*cov_sor@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)*stdPort**(-3)*cov_sor@w |
||||
return g1-g2 |
||||
# equivalent opt problem with min vol |
||||
@staticmethod |
||||
def volatility(w, ret): |
||||
retPort = ret@w # T-dimensional array |
||||
return np.std(retPort) |
||||
@staticmethod |
||||
def volatility_grad(w, ret, cov): |
||||
retPort = ret@w # T-dimensional array |
||||
stdPort = np.std(retPort) |
||||
return cov@w/stdPort |
||||
@staticmethod |
||||
def quadratic_utility(w, ret, gamma): |
||||
retPort = ret@w # T-dimensional array |
||||
varPort = np.var(retPort) |
||||
return np.mean(retPort) - 0.5*gamma*varPort |
||||
@staticmethod |
||||
def quadratic_utility_grad(w, ret, cov, gamma): |
||||
manual_ret = np.mean(ret, axis=0) |
||||
return manual_ret - gamma*cov@w |
||||
@classmethod |
||||
def opt(cls, ret, gamma=0, 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) |
||||
elif role=="quadratic_utility": |
||||
cov=np.cov(ret.T) |
||||
loss = lambda w: -cls.quadratic_utility(w, ret, gamma) |
||||
grad = lambda w: -cls.quadratic_utility_grad(w, ret, cov, gamma) |
||||
else: |
||||
return init |
||||
bnds = [[0, 0.6] for i in range(n)] |
||||
opts = {'maxiter': 1000, '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,16 @@ |
||||
Flask==2.2.2 |
||||
Flask-Login==0.6.2 |
||||
Flask-SQLAlchemy==3.0.3 |
||||
Flask-Caching==2.0.2 |
||||
redis==4.5.3 |
||||
gunicorn==20.0.4 |
||||
packaging==23.0 |
||||
pandas==1.5.3 |
||||
plotly==5.13.1 |
||||
psycopg2==2.9.5 |
||||
pytz==2022.7.1 |
||||
requests==2.28.2 |
||||
scipy==1.10.0 |
||||
SQLAlchemy==2.0.4 |
||||
gevent==22.10.2 |
||||
yfinance==0.2.22 |
||||
@ -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,3 @@ |
||||
FROM postgres:15.3-alpine |
||||
|
||||
COPY create_all.sql /docker-entrypoint-initdb.d/ |
||||
@ -0,0 +1,59 @@ |
||||
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 |
||||
); |
||||
CREATE INDEX idx_username ON users (username); |
||||
DROP TABLE IF EXISTS stock_price; |
||||
DROP TABLE IF EXISTS stock_price_tw; |
||||
|
||||
CREATE TABLE stock_price ( |
||||
id SERIAL PRIMARY KEY, |
||||
ticker VARCHAR(32) NOT NULL, |
||||
date DATE NOT NULL, |
||||
price 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(32) NOT NULL, |
||||
date DATE NOT NULL, |
||||
price REAL NOT NULL |
||||
); |
||||
-- you need to add () |
||||
CREATE INDEX idx_ticker_tw ON stock_price_tw (ticker); |
||||
|
||||
DROP TABLE IF EXISTS strategy; |
||||
CREATE TABLE strategy ( |
||||
id SERIAL PRIMARY KEY, |
||||
date VARCHAR(64) NOT NULL, |
||||
name VARCHAR(32) NOT NULL, |
||||
username VARCHAR(32) NOT NULL, |
||||
competition VARCHAR(32) NOT NULL, |
||||
role VARCHAR(20) 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, |
||||
gamma REAL NOT NULL, |
||||
tw BOOLEAN DEFAULT TRUE, |
||||
notes VARCHAR(255), |
||||
assets TEXT[] NOT NULL, |
||||
weight JSON NOT NULL, |
||||
ret JSON NOT NULL, |
||||
comments TEXT[][] |
||||
); |
||||
CREATE INDEX idx_user ON strategy (username); |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,21 @@ |
||||
DROP TABLE IF EXISTS stock_price; |
||||
DROP TABLE IF EXISTS stock_price_tw; |
||||
|
||||
|
||||
CREATE TABLE stock_price ( |
||||
id SERIAL PRIMARY KEY, |
||||
ticker VARCHAR(32) NOT NULL, |
||||
date DATE NOT NULL, |
||||
price 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(32) NOT NULL, |
||||
date DATE NOT NULL, |
||||
price REAL NOT NULL |
||||
); |
||||
-- you need to add () |
||||
CREATE INDEX idx_ticker_tw ON stock_price_tw (ticker); |
||||
@ -0,0 +1,26 @@ |
||||
DROP TABLE IF EXISTS strategy; |
||||
CREATE TABLE strategy ( |
||||
id SERIAL PRIMARY KEY, |
||||
date VARCHAR(64) NOT NULL, |
||||
name VARCHAR(32) NOT NULL, |
||||
username VARCHAR(32) NOT NULL, |
||||
competition VARCHAR(32) NOT NULL, |
||||
role VARCHAR(20) 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, |
||||
gamma REAL NOT NULL, |
||||
tw BOOLEAN DEFAULT TRUE, |
||||
notes VARCHAR(255), |
||||
assets TEXT[] NOT NULL, |
||||
weight JSON NOT NULL, |
||||
ret JSON NOT NULL, |
||||
comments TEXT[][] |
||||
); |
||||
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 |
||||
); |
||||
CREATE INDEX idx_username ON users (username); |
||||
INSERT INTO users (username, password) |
||||
VALUES ('R10246002@ntu.edu.tw', 'pbkdf2:sha256:260000$Z5bK5pp0D8HEDAps$abb43b1b1c543ff334de8fb3aeba9c0460c37de5b5363e5210e68b00739d5e2c'); |
||||
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 1015 B |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 765 KiB |
@ -0,0 +1,174 @@ |
||||
// Initialize empty stock list
|
||||
// let stockList = ['2330.TW'];
|
||||
var stockList = []; |
||||
$('#stock-list span').each(function(){ |
||||
stockList.push($(this).text()); |
||||
}); |
||||
console.log(stockList); |
||||
let currentList = []; |
||||
const layout={'autosize': true, 'markers':true, |
||||
'title': {'text': ''},
|
||||
'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0], 'rangeslider': {'visible': true}},
|
||||
'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0]}, |
||||
'legend': {'yanchor': 'top', 'y': 1.1, 'xanchor': 'left', 'x': 0.01, 'orientation':'h'}, |
||||
'margin': {'l': 25, 'r': 5, 't': 10, 'b': 5}, |
||||
} |
||||
// Cache frequently-used DOM elements
|
||||
const $stockForm = $('#stock-form'); |
||||
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 changeFunc(value) { |
||||
console.log(value); |
||||
if (value === 'quadratic_utility') { |
||||
$('#gamma').css("display", "flex"); |
||||
} else { |
||||
$('#gamma').css("display", "none"); |
||||
} |
||||
} |
||||
// 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);
|
||||
}); |
||||
// Event listener for submit button click
|
||||
$addStockBtn.click(function(event) { |
||||
event.preventDefault(); |
||||
// console.log($('input[name=assetSelect]').val())
|
||||
// Get selected stock from form
|
||||
var text = $('input[name=assetSelect]').val(); |
||||
// const selectedStock = text;
|
||||
// var text = $('#stock-select option:selected').text();
|
||||
// console.log(text)
|
||||
if (text != null && text!= '' && stockList.indexOf(text)===-1) { |
||||
// Add new item to list
|
||||
addStockItem(text, text); |
||||
|
||||
// Clear input field
|
||||
$('#stockAll').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').replaceWith("<span>投資組合已開始建立,請等待完成訊息,或1分鐘後至分析結果區查看!</span>")
|
||||
// $('#confirmModal').modal('show');
|
||||
$submitPort.prop('disabled', true); |
||||
$.ajax({ |
||||
url: '/postPort', //todo create_strategy
|
||||
method: 'POST', |
||||
data: {
|
||||
name: $('input[name=portName]').val(), |
||||
ts: Date(Date.now()),
|
||||
comp: $('#competition').val(), |
||||
lookback: $('#lookback').val(), |
||||
frequency: $('#opt-frequency').val(), |
||||
role: $('#role-select').val(), |
||||
gamma: $('#util-gamma').val(), |
||||
comment: $commentPort.val(), |
||||
stockList: JSON.stringify(stockList) |
||||
}, |
||||
success: function(response) { |
||||
// console.log(response);
|
||||
// var res = JSON.parse(response);
|
||||
event.preventDefault(); |
||||
// $('#modalTitle').text('完成建立')
|
||||
$('#sucMes').html(response); |
||||
// <span>投資組合建立時間間隔(或與登入時間間隔)必須大於60秒</span>
|
||||
$('#confirmModal').modal('show');
|
||||
|
||||
$submitPort.prop('disabled', false); |
||||
}, |
||||
error: function(xhr) { |
||||
console.log('Error submitting stock list: ' + xhr.responseText); |
||||
alert('建立失敗,請確認資產名稱是否正確! 美股代號均為大寫、台股代號為數字後接".TW"或是"TWO"'); |
||||
} |
||||
}); |
||||
$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);
|
||||
var texts = []; |
||||
$('#stock-list span').each(function(){ |
||||
texts.push($(this).text()); |
||||
}); |
||||
// alert(currentList.includes(texts));
|
||||
if (stockList.length > 0 && JSON.stringify(currentList)!==JSON.stringify(stockList)) { |
||||
// cacheList = stockList;
|
||||
$('#graph').html('<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div>') |
||||
|
||||
$.ajax({ |
||||
url: '/postStock', //todo create_strategy
|
||||
method: 'POST', |
||||
data: { stockList: JSON.stringify(stockList) }, |
||||
success: function(response) { |
||||
$('#graph').html('') |
||||
var graphs = JSON.parse(response); |
||||
// console.log(graphs.data);
|
||||
Plotly.newPlot("graph", |
||||
graphs.data, layout, {responsive: true}); |
||||
// console.log(response.layout);
|
||||
currentList = stockList.map(obj => obj); |
||||
|
||||
}, |
||||
error: function(xhr) { |
||||
$('#graph').html('<div><span class="badge bg-warning">錯誤</span></div>') |
||||
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) |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,14 @@ |
||||
<!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>Document</title> |
||||
</head> |
||||
<body style="background-image: linear-gradient(to bottom right, #5d9faa , #c4e0e5);min-height:92%;"> |
||||
<div class="container-fluid justify-content-center"> |
||||
<h1 class="font-bold text-4xl">404 Error</h1> |
||||
</div> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,427 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="zh-Hant-TW"> |
||||
<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 %}TPM{% endblock title%}</title> |
||||
<link rel="shortcut icon" type="image/x-icon" href="{{ url_for('static', filename='img/growth.ico') }}"> |
||||
<link |
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css" |
||||
rel="stylesheet" |
||||
integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp" |
||||
crossorigin="anonymous"> |
||||
<link href="https://getbootstrap.com/docs/5.3/assets/css/docs.css" rel="stylesheet"> |
||||
<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-bottom: 10px; */ |
||||
font-family: Georgia, Arial, Helvetica, sans-serif !important; |
||||
background-color: #eee; |
||||
height:95vh; |
||||
} |
||||
.navbar { |
||||
background-image: linear-gradient(to bottom right, #5d9faa , #c4e0e5); |
||||
} |
||||
.navbar-nav > li > a.active { |
||||
font-size: 18px; |
||||
border-bottom: 2px ridge #888888; |
||||
} |
||||
|
||||
.footer { |
||||
position: absolute; |
||||
bottom: 0; |
||||
width: 100%; |
||||
/* Set the fixed height of the footer here */ |
||||
height: 60px; |
||||
background-color: #f5f5f5; |
||||
} |
||||
.navbar-fixed-top { |
||||
top:20px; |
||||
} |
||||
{% block style %} |
||||
p { |
||||
text-indent: 2em; |
||||
} |
||||
{% endblock %} |
||||
</style> |
||||
|
||||
</head> |
||||
<header> |
||||
|
||||
{% set navigation_bar = [ |
||||
('/', 'index', '首頁', 'bi bi-house-fill'), |
||||
('/strategy', 'strategy', '建立策略', 'fa-solid fa-chart-pie'), |
||||
('/strategy_tw', 'strategy_tw', '台股建立策略', 'fa-solid fa-chart-pie'), |
||||
('/custom', 'custom', '自訂數據建立策略', 'bi bi-database-fill-add'), |
||||
('/result', 'result', '分析結果排行', 'fa-solid fa-chart-simple'), |
||||
('mailto:r10246002@ntu.edu.tw', 'error', '錯誤回報', 'bi bi-bug-fill') |
||||
] -%} |
||||
<!-- ('/', 'strategy_bl', 'Black-Litterman配置', 'fa-solid fa-chess-knight'), --> |
||||
{% set active_page = active_page|default('index') -%} |
||||
<nav class="navbar bg-light"> |
||||
<div class="container-fluid p-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"> |
||||
{% 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">歡迎您: {{ 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 %} |
||||
<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="fa-solid fa-globe"></i> |
||||
</a> |
||||
<ul class="dropdown-menu dropdown-menu-end"> |
||||
<li> |
||||
<div id="google_translate_element"></div> |
||||
</li> |
||||
</ul> |
||||
</li> |
||||
</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-end text-bg-light" |
||||
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 %} |
||||
<script type="text/javascript" src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script> |
||||
<div class="container-lg mx-auto my-2 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> --> |
||||
{% 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" role="alert"> |
||||
{{ message }} |
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> |
||||
</div> |
||||
{% endfor %} |
||||
{% endif %} |
||||
{% endwith %} |
||||
{% cache 300 %} |
||||
<div class="prose lg:prose-xl p-3"> |
||||
<h2 class="text-xl font-bold mb-4">平台簡介</h2> |
||||
<hr class="mt-0 mb-2"> |
||||
<p class="font-bold"> |
||||
「不要把所有的雞蛋放在一個籃子裡。」 |
||||
</p> |
||||
<p> |
||||
分散投資是一種投資策略,旨在通過將資金分散投資在不同的資產類別中,從而降低投資風險。 |
||||
如果兩個資產之間存在高度正相關,則它們的價格通常會同時上漲或下跌。 |
||||
相反,如果它們之間存在高度負相關,則它們的價格通常會發生相反的變化。 |
||||
因此,分散投資應選擇具有低相關性的資產,以實現風險的分散。 |
||||
現代投資組合理論(Modern Portfolio Theory, MPT)由哈利·馬科維茨(Harry Markowitz) |
||||
於1952年首次提出的,他認為投資者應該根據風險和回報的平衡來選擇投資組合。他提出了“有效前緣”(Efficient Frontier)的概念, |
||||
|
||||
</p> |
||||
<p> |
||||
Efficient Frontier的建立基於資產收益率和風險的統計分析和優化理論。資產收益率可以通過歷史數據進行計算,而風險則通常使用標準差、協方差等統計指標進行衡量。 |
||||
|
||||
假設有n個資產,它們的收益率分別為 $R_1, R_2, ..., R_n$,協方差矩陣為 $\Sigma$。假設現有一個投資組合,其中各資產的權重分別為 $w_1, w_2, ..., w_n$,則該投資組合的收益率為: |
||||
</p> |
||||
<span style="font-size: min(2.5vmin, 20px);"> |
||||
$$R_p = w_1 R_1 + w_2 R_2 + ... + w_n R_n = w^T R$$ |
||||
</span> |
||||
|
||||
<p> |
||||
該投資組合的方差為: |
||||
</p> |
||||
<span style="font-size: min(2.5vmin, 20px);"> |
||||
$$\begin{align}\sigma^2_p |
||||
&= w_1^2 \sigma_1^2 + w_2^2 \sigma_2^2 + ... |
||||
\\&+ w_n^2 \sigma_n^2 + 2w_1w_2\sigma_{1,2} + |
||||
2w_1w_3\sigma_{1,3} + ... \\&+ 2w_{n-1}w_n\sigma_{n-1,n} = w^T\Sigma w\end{align}$$ |
||||
</span> |
||||
<p> |
||||
其中,$\sigma_i$ 表示第 i 個資產的標準差,$\sigma_{i,j}$ 表示第 i 和第 j 個資產的協方差。 |
||||
|
||||
為了尋找最優投資組合,我們可以使用均值-方差優化方法。該方法的核心思想是通過最小化投資組合的方差,來最大化其收益率。假設有一個投資者的風險偏好係數為 $\gamma$,則該投資者所選擇的最優投資組合為: |
||||
</p> |
||||
<span style="font-size: min(2.5vmin, 20px);"> |
||||
$$\text{min}\ \frac{1}{2}w^T \Sigma w - \gamma w^T R$$ |
||||
|
||||
$$\text{s.t.}\ \sum_{i=1}^n w_i = 1, w_i \geq 0$$ |
||||
</span> |
||||
|
||||
<p> |
||||
其中,$w$ 是資產權重向量,$\Sigma$ 是協方差矩陣,$R$ 是收益率向量。目標函數是投資組合方差的一半減去風險偏好係數乘以投資組合收益率,約束條件是權重向量的元素之和為1且每個權重都不小於0。 |
||||
|
||||
通過調整風險偏好係數 $\gamma$ |
||||
,可以獲得不同風險水平下的最優投資組合,這些投資組合形成了有效邊界(Efficient Frontier),即在一定風險水平下,可以實現最大化收益的投資組合集合。 |
||||
</p> |
||||
<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> |
||||
在實際應用中,通常使用投資組合的夏普比率(Sharpe Ratio)作為評估指標。夏普比率可以看作是投資組合每單位風險所帶來的超額收益,其計算公式為: |
||||
</p> |
||||
<span style="font-size: min(2.5vmin, 20px);"> |
||||
$$\text{Sharpe Ratio} = \frac{R_p - R_f}{\sigma_p}$$ |
||||
</span> |
||||
<p> |
||||
其中,$R_p$ 是投資組合的預期收益率,$R_f$ 是無風險利率,$\sigma_p$ 是投資組合的標準差。 |
||||
|
||||
Efficient Frontier的建立是現代投資組合理論的基石之一。通過將不同資產的收益率和風險納入考慮, |
||||
投資者可以通過構建有效邊界,實現在不同風險水平下的最優資產配置。 |
||||
這為投資者提供了一個更有效的投資方案,可以實現更穩定的收益和更低的風險水平。 |
||||
</p> |
||||
|
||||
|
||||
|
||||
<p class="p-0"> |
||||
------ |
||||
</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: min(2.5vmin, 20px);"> |
||||
$$\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: min(2.5vmin, 20px);"> |
||||
$$\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> |
||||
<h2 class="text-xl font-bold mb-2 mt-4">投資組合的$\alpha$、$\beta$值</h2> |
||||
<hr class="mt-1 mb-3"> |
||||
<p class="my-1"> |
||||
投資組合報酬率的Alpha和Beta是投資組合評估中常用的指標。 |
||||
Alpha代表投資組合的超額收益,Beta則代表投資組合與市場的相關性。 |
||||
Beta值是衡量資產相對於整個市場的波動性的指標。Beta值的公式如下: |
||||
</p> |
||||
<span style="font-size: min(2.5vmin, 20px);"> |
||||
$$\beta_i = \frac{\text{Cov}(r_i,r_m)}{\text{Var}(r_m)}$$ |
||||
</span> |
||||
<p> |
||||
其中,$r_i$表示資產i的收益率,$r_m$表示市場收益率, |
||||
$Cov(r_i,r_m)$表示資產i的收益率和市場收益率之間的協方差, |
||||
$Var(r_m)$表示市場收益率的方差。Beta值越高,表示資產的波動性越大, |
||||
其收益率與市場收益率之間的關聯度也越高。 |
||||
Beta值可以用來評估投資組合的風險程度,投資者可以通過控制投資組合中資產的Beta值來實現風險管理。 |
||||
Beta值可以與CAPM(Capital Asset Pricing Model)和線性回歸相關聯。在CAPM中,假設資產的預期收益率可以通過以下公式計算: |
||||
</p> |
||||
<span style="font-size: min(2.5vmin, 20px);"> |
||||
$$E(r_i) = r_f + \beta_i(E(r_m) - r_f)$$ |
||||
</span> |
||||
<p> |
||||
|
||||
其中,$E(r_i)$表示資產i的預期收益率,$r_f$表示無風險收益率,$E(r_m)$表示市場的預期收益率, |
||||
$\beta_i$表示資產i的Beta值。該公式表示,資產的預期收益率是無風險收益率和市場風險溢價的加權平均值, |
||||
其中市場風險溢價的大小與市場風險的大小成正比,且與資產的Beta值相關。 |
||||
</p> |
||||
<p class="my-1"> |
||||
Alpha 值是指投資組合的實際收益率與其根據 Beta 值預期的收益率之間的差異。如果投資組合的實際收益率高於其根據 Beta 值所預期的收益率,則 Alpha 值為正;反之,如果投資組合的實際收益率低於其預期收益率,則 Alpha 值為負。 |
||||
|
||||
Alpha 值可以通過線性回歸分析來計算。假設投資組合的收益率可以表示為以下公式: |
||||
</p> |
||||
<span style="font-size: min(2.5vmin, 20px);"> |
||||
$$r_p = \alpha + \beta_p (r_m-f_f) + \epsilon$$ |
||||
</span> |
||||
<p> |
||||
其中,$r_p$ 表示投資組合的收益率,$\alpha$ 表示 Alpha 值,$\beta_p$ 表示投資組合的 Beta 值,$r_m$ 表示市場收益率,$\epsilon$ 表示誤差項。如果我們將上述公式進行線性回歸,可以得到: |
||||
</p> |
||||
<span style="font-size: min(2.5vmin, 20px);"> |
||||
$$\hat{r_p} = \hat{\alpha} + \hat{\beta_p} (r_m-r_f)$$ |
||||
</span> |
||||
<p> |
||||
|
||||
|
||||
其中,$\hat{r_p}$ 表示預測的投資組合收益率,$\hat{\alpha}$ 表示預測的 Alpha 值,$\hat{\beta_p}$ 表示預測的 Beta 值。如果實際收益率高於預測收益率,則 $\hat{\alpha}$ 為正;反之,如果實際收益率低於預測收益率,則 $\hat{\alpha}$ 為負。 |
||||
</p> |
||||
<h2 class="text-xl font-bold mb-2 mt-4">VaR 簡介</h2> |
||||
<hr class="mt-1 mb-3"> |
||||
<p class="my-1"> |
||||
VaR (Value at Risk) 是用來衡量投資組合或資產在一定時間內可能面臨的最大潛在損失的一種風險管理指標。VaR 通常用機率的形式表達,表示在一定信心水平下,資產或投資組合在未來一定時間內的最大可能損失額。 |
||||
|
||||
假設 $X$ 是一個隨機變量,表示資產或投資組合在未來一定時間內的損失額,$p$ 表示所選取的信心水平,VaR 可以表示為: |
||||
</p> |
||||
<span style="font-size: min(2.5vmin, 20px);"> |
||||
$$\text{VaR}_p(X) = - \inf \{ x \in \mathbb{R} : F_X(x) \geq p \}$$ |
||||
</span> |
||||
<p> |
||||
其中,$F_X(x)$ 是 $X$ 的累積分布函數。這個公式的意思是,我們尋找一個最小的數值 $x$,使得資產或投資組合在未來一定時間內的損失額大於 $x$ 的機率不超過 $1-p$。這個最小的數值就是 VaR,通常表示為負數,因為它是損失額。 |
||||
|
||||
例如,假設我們選取信心水平 $p=0.95$,並且假設資產或投資組合在未來一周內的報酬率的分布是正態分布 $N(\mu,\sigma^2)$,那麼根據上述公式,VaR 的值為: |
||||
</p> |
||||
<span style="font-size: min(2.5vmin, 20px);"> |
||||
$$\begin{align}\text{VaR}_{0.95}(X) &= -\inf\{ x \in \mathbb{R} : F_X(x) \leq 0.05 \} \\ |
||||
&= -\inf \{x \in \mathbb{R} : \mathbb{P}(z\leq\frac{x-\mu}{\sigma}) \leq 0.05) \} \\ |
||||
&= -\inf \{x \in \mathbb{R} : \Phi (\frac{x-\mu}{\sigma}) \leq 0.05 \} \\ |
||||
&= -(\mu - 1.645\times\sigma)\end{align}$$ |
||||
</span> |
||||
<p> |
||||
|
||||
|
||||
其中,$\Phi(x)$ 是標準常態分布的累積分布函數。 |
||||
需要注意的是,VaR 是一個單一數字,只反映了資產或投資組合在一定時間內的最大可能損失額, |
||||
不能反映風險的分布情況。因此,在使用 VaR 進行風險管理時, |
||||
還需要結合其他風險指標和風險管理方法來進行綜合分析和決策。 |
||||
</p> |
||||
</div> |
||||
{% endcache %} |
||||
</div> |
||||
{% endblock content %} |
||||
{% cache 300 %} |
||||
<footer id="footer" class="text-center text-white" style="background-color: #6171ce;margin-bottom: 0"> |
||||
<div |
||||
class="text-center p-3" |
||||
style="background-color: rgba(0, 0, 0, 0.2)" |
||||
> |
||||
© 2023 Copyright NTHU-TPM |
||||
<a href="https://github.com/SeanChenTaipei" target="_blank" rel="noopener" class="btn-outline-light btn-floating" role="button"> |
||||
<i class="fab fa-github"></i> |
||||
</a> |
||||
</div> |
||||
<!-- Copyright --> |
||||
</footer> |
||||
{% endcache %} |
||||
<!--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-alpha2/dist/js/bootstrap.bundle.min.js" |
||||
integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N" |
||||
crossorigin="anonymous"> |
||||
</script> |
||||
<script type="text/javascript"> |
||||
function googleTranslateElementInit() { |
||||
new google.translate.TranslateElement({pageLanguage: 'zh-TW', |
||||
layout: google.translate.TranslateElement.InlineLayout.HORIZONTAL}, 'google_translate_element'); |
||||
} |
||||
</script> |
||||
<script src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></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,54 @@ |
||||
<option value="none" selected="selected">無</option> |
||||
<option value="fintech_ai_411">411 AI日</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,91 @@ |
||||
{% extends 'base.html' %} |
||||
{% set active_page = 'custom' %} |
||||
|
||||
|
||||
{% block title %}Strategy Page{% endblock%} |
||||
{% block content %} |
||||
{% cache 300 %} |
||||
<div class="container-fluid" style="min-height:92%;position:relative;"> |
||||
<div class="card my-3"> |
||||
<div class="card-header"> |
||||
<h1 class="modal-title font-bold text-xl" style="color: #000055;">格式規範與上傳檔案</h1> |
||||
</div> |
||||
<div class="card-body"> |
||||
<div class="row justify-content-center"> |
||||
<div class="col-lg-5 col-md-5 col-sm-10"> |
||||
<ul class="fa-ul"> |
||||
<li><span class="fa-li"><i class="fa-solid fa-flag"></i></span>上傳之csv檔需包含header,且第一行為時間資訊。</li> |
||||
<li><span class="fa-li"><i class="fa-solid fa-flag"></i></span>價格資訊需長度相同,且資產數量大於1檔才會進行回測。</li> |
||||
<li><span class="fa-li"><i class="fa-solid fa-flag"></i></span>範例如下圖所示。</li> |
||||
</ul> |
||||
<img src="{{ url_for('static', filename='img/file.jpg') }}" class="img-fluid mb-3" alt="SINGUP IMAGE"> |
||||
</div> |
||||
<div class="col-lg-7 col-md-7 col-sm-10"> |
||||
<form method="POST" enctype="multipart/form-data"> |
||||
<div class="p-2 font-bold text-lg"> |
||||
投組最佳化配置 |
||||
</div> |
||||
<div class="input-group"> |
||||
<span class="input-group-text bg-info">滾動視窗大小</span> |
||||
<select name="lookback" class="form-select"> |
||||
<option value="21">1個月</option> |
||||
<option value="63">3個月</option> |
||||
<option selected value="126">6個月</option> |
||||
<option value="252">12個月</option> |
||||
</select> |
||||
</div> |
||||
<div class="input-group"> |
||||
<span class="input-group-text bg-info">最佳化頻率</span> |
||||
<select name="frequency" class="form-select"> |
||||
<option value="21">每月</option> |
||||
<option value="63">每季</option> |
||||
<option selected value="126">每半年</option> |
||||
<option value="252">每年</option> |
||||
</select> |
||||
</div> |
||||
<div class="input-group"> |
||||
<span class="input-group-text bg-info">最佳化目標函數</span> |
||||
<select name="role" class="form-select" onchange="changeFunc(value);"> |
||||
<option selected value="max_sharpe">最大化夏普比率</option> |
||||
<option value="max_sortino">最大化索提諾比率</option> |
||||
<option value="min_volatility">最小化波動率</option> |
||||
<option value="quadratic_utility">最大化效用函數</option> |
||||
</select> |
||||
</div> |
||||
<div class="input-group" style="display: none;" id="gamma"> |
||||
<span class="input-group-text bg-info">風險厭惡係數</span> |
||||
<input type="number" id="gamma" name="gamma" name="targetAnnualVolatility" class="form-control fmt-pct" value="30" autocomplete="off"> |
||||
<span class="input-group-text">%</span> |
||||
</div> |
||||
<div class="form-group d-flex mt-3"> |
||||
<input type="file" class="form-control-file" id="csv_file" name="csv_file" accept=".csv" max-file="3" required> |
||||
<button id="uploadCheck" type="submit" class="btn btn-outline-primary ms-auto">確認上傳</button> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endcache %} |
||||
{% endblock %} |
||||
{% block script %} |
||||
<script> |
||||
function changeFunc(value) { |
||||
console.log(value); |
||||
if (value === 'quadratic_utility') { |
||||
$('#gamma').css("display", "flex"); |
||||
} else { |
||||
$('#gamma').css("display", "none"); |
||||
} |
||||
} |
||||
</script> |
||||
<script> |
||||
$("#csv_file").on("change", function () { |
||||
if(this.files[0].size > 1000000) { |
||||
alert("檔案大小請勿超過 1MB !!"); |
||||
$(this).val(null); |
||||
} |
||||
}); |
||||
</script> |
||||
{% endblock script %} |
||||
@ -0,0 +1,70 @@ |
||||
{% 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" style="background-color: #eee;min-height:92%;position:relative;"> |
||||
<div class="container-fluid 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 mt-3" 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-2">登入</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" 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 input-group-lg"> |
||||
<label for="InputEmail1" class="form-label">Email address</label> |
||||
<input type="email" class="form-control input-lg" 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 input-group-lg"> |
||||
<label for="InputPassword1" class="form-label">Password</label> |
||||
<input type="password" class="form-control input-lg" id="InputPassword1" name="password" required> |
||||
</div> |
||||
<div class="m-3 form-check input-group-lg"> |
||||
<input type="checkbox" class="form-check-input input-lg" 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> |
||||
|
||||
{% endblock %} |
||||
@ -0,0 +1,70 @@ |
||||
{% extends 'base.html' %} |
||||
{% set active_page = none %} |
||||
|
||||
|
||||
{% block title %}Registration Page{% endblock %} |
||||
|
||||
{% block style %} |
||||
|
||||
{% endblock style %} |
||||
{% block content %} |
||||
<section class="container-fluid" style="background-color: #eee;min-height:100%;position:relative;"> |
||||
<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 input-group-lg"> |
||||
<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 input-group-lg"> |
||||
<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 input-group-lg"> |
||||
<label for="InputPassword2" class="form-label">Repeat 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> |
||||
</div> |
||||
{% endblock %} |
||||
@ -0,0 +1,99 @@ |
||||
{% extends 'base.html' %} |
||||
{% set active_page = 'result' %} |
||||
|
||||
{% block style %} |
||||
.card-header { |
||||
color: #000093; |
||||
} |
||||
{% endblock style %} |
||||
|
||||
{% block title %}Result Page{% endblock%} |
||||
{% block content %} |
||||
<div class="container-fluid" style="background-color: #eee;min-height:92%;position:relative;"> |
||||
<div class="container-fluid px-1 py-4"> |
||||
<div class="alert alert-light p-3 mx-3" role="alert" style="border: 1px solid black"> |
||||
<div class="flex-row"> |
||||
<p class="font-italic font-bold text-xl"> |
||||
"Risk comes from not knowing what you're doing." |
||||
</p> |
||||
</div> |
||||
<div class="d-flex flex-row-reverse"> |
||||
<p class="align-self-end text-xs">- Warren Edward Buffett</p> |
||||
</div> |
||||
|
||||
<hr class="my-3 px-5"> |
||||
<form method="POST"> |
||||
<div class="input-group mb-1"> |
||||
<span class="input-group-text" id="in">選擇競賽</span> |
||||
<select id="competition" class="form-select" size="1" name="competition"> |
||||
{% include 'competitions.html' %} |
||||
</select> |
||||
</div> |
||||
<div class="input-group mb-3"> |
||||
<span class="input-group-text">排序方式</span> |
||||
<select id="rrr" class="form-select" size="1" name="role"> |
||||
<option value="id">時間排序</option> |
||||
<option value="annual_ret">報酬率排序</option> |
||||
<option value="annual_sr">夏普率排序</option> |
||||
<option value="vol">波動率排序</option> |
||||
<option value="my">我的策略</option> |
||||
</select> |
||||
<div class=""> |
||||
<button id="changeComp" type="submit" class="btn btn-secondary"><i class="bi bi-arrow-right-square-fill"></i></button> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
<div class="card m-3"> |
||||
<div card="card p-3 m-3"> |
||||
<div class="card-header"> |
||||
<ul class="fa-ul"> |
||||
<li><span class="fa-li"><i class="fa-solid fa-flag"></i></span>點擊ID查看詳細資訊。</li> |
||||
<li><span class="fa-li"><i class="fa-solid fa-flag"></i></span>預設依策略建立時間排序方式。</li> |
||||
<li><span class="fa-li"><i class="fa-solid fa-flag"></i></span>資料庫每分鐘進行更新,若策略未顯示,請一分鐘後刷新頁面。</li> |
||||
</ul> |
||||
</div> |
||||
<div class="p-3 table-responsive-sm table-responsive-md table-responsive-xl"> |
||||
<table class="table caption-top"> |
||||
<thead> |
||||
<tr> |
||||
<th scope="col">#ID</th> |
||||
<th scope="col">投組名稱</th> |
||||
<th scope="col">創建者</th> |
||||
<th scope="col">報酬率</th> |
||||
<th scope="col">夏普率</th> |
||||
<th scope="col">波動率</th> |
||||
<th scope="col">最大回落</th> |
||||
<th scope="col">創建時間</th> |
||||
</tr> |
||||
</thead> |
||||
{% for info in strategy_data %} |
||||
<thead style="font-size: 1vmin'"> |
||||
<tr> |
||||
<th scope="col" role="alert"> |
||||
<a href="{{ url_for('result_view', strategy_id=info[0]) }}" class="alert-link"> |
||||
<span class="badge rounded-pill text-bg-info">{{ info[0] }}</span> |
||||
</a> |
||||
</th> |
||||
<td>{{ info[2] }}</td> |
||||
<td>{{ info[3] }}</td> |
||||
<td>{{ info[4] }}</td> |
||||
<td>{{ info[6] }}</td> |
||||
<td>{{ info[5] }}</td> |
||||
<td>{{ info[7] }}</td> |
||||
<td>{{ info[1]|truncate(16, False, '', 0) }}</td> |
||||
</tr> |
||||
</thead> |
||||
{% endfor %} |
||||
</table> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
{% endblock %} |
||||
{% block script %} |
||||
|
||||
|
||||
{% endblock script %} |
||||
@ -0,0 +1,194 @@ |
||||
{% extends 'base.html' %} |
||||
|
||||
|
||||
{% block title %}Result View{% endblock %} |
||||
{% block style %} |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
<div class="container-fluid" style="background-color: #eee;min-height:92%;position:relative;"> |
||||
<div class="container-fluid 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="alert alert-light text-dark" role="alert"> |
||||
<div class="container"> |
||||
<div class="row"> |
||||
<div class="col-4"> |
||||
<strong>策略編號</strong> |
||||
</div> |
||||
<div class="col-6"> |
||||
{{ data.id|safe }} |
||||
</div> |
||||
</div> |
||||
<hr> |
||||
<div class="row"> |
||||
<div class="col-4"> |
||||
<strong>策略名稱</strong> |
||||
</div> |
||||
<div class="col-6"> |
||||
{{ data.name|safe }} |
||||
</div> |
||||
</div> |
||||
<hr> |
||||
<div class="row"> |
||||
<div class="col-4"> |
||||
<strong>建立者</strong> |
||||
</div> |
||||
<div class="col-6"> |
||||
{{ data.username|safe }} |
||||
</div> |
||||
</div> |
||||
<hr> |
||||
<div class="row"> |
||||
<div class="col-4"> |
||||
<strong>建立時間</strong> |
||||
</div> |
||||
<div class="col-6"> |
||||
{{ data.date|safe }} |
||||
</div> |
||||
</div> |
||||
<hr> |
||||
<div class="row"> |
||||
<div class="col-4"> |
||||
<strong>策略目標</strong> |
||||
</div> |
||||
<div class="col-6"> |
||||
{{ data.role|safe }} |
||||
</div> |
||||
</div> |
||||
<hr> |
||||
<div class="row"> |
||||
<div class="col-4"> |
||||
<strong>資產</strong> |
||||
</div> |
||||
<div class="col-6"> |
||||
{% for a in data.assets %} |
||||
<span class="badge text-bg-warning asset" name="stock" value="{{ a|safe }}">{{ a|safe }}</span> |
||||
{% endfor %} |
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('copy_portfolio') }}"> |
||||
複製投資組合 |
||||
</a> |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<button class="btn btn-secondary mt-3" type="button" data-bs-toggle="collapse" data-bs-target="#collapse1" aria-expanded="false" aria-controls="collapseExample"> |
||||
詳細資訊 |
||||
</button> |
||||
<div class="collapse" id="collapse1"> |
||||
<div card="card card-body m-3"> |
||||
<div class="p-1 table-responsive-sm table-responsive-md table-responsive-xl"> |
||||
<table class="table caption-top"> |
||||
<thead> |
||||
<tr> |
||||
<th scope="col">年化報酬率</th> |
||||
<th scope="col">年化夏普率</th> |
||||
<th scope="col">年化波動率</th> |
||||
<th scope="col">最大回落</th> |
||||
<th scope="col">Alpha</th> |
||||
<th scope="col">Beta</th> |
||||
<th scope="col">VaR10</th> |
||||
<th scope="col">R2</th> |
||||
{% if data.role == '最大化效用函數' %} |
||||
<th scope="col">Gamma</th> |
||||
{% endif %} |
||||
</tr> |
||||
</thead> |
||||
<thead style="font-size: 1vmin'"> |
||||
<tr> |
||||
<td>{{ data.annual_ret }}</td> |
||||
<td>{{ data.annual_sr }}</td> |
||||
<td>{{ data.vol }}</td> |
||||
<td>{{ data.mdd }}</td> |
||||
<td>{{ data.alpha }}</td> |
||||
<td>{{ data.beta }}</td> |
||||
<td>{{ data.var10 }}</td> |
||||
<td>{{ data.r2 }}</td> |
||||
{% if data.role == '最大化效用函數' %} |
||||
<td>{{ data.gamma }}</td> |
||||
{% endif %} |
||||
</tr> |
||||
</thead> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="card mt-3 py-2" style="border-radius: 10px;"> |
||||
<div class="card-body p-0" style="border-radius: 10px;"> |
||||
<div class="row justify-content-center font-bold text-xl"> |
||||
資產權重變化 |
||||
</div> |
||||
<div class="mb-4" id="weight" style="max-height:50vh"></div> |
||||
<div class="row justify-content-center font-bold text-xl"> |
||||
投組價值走勢 |
||||
</div> |
||||
<div class="mb-4" id="price" style="max-height:60vh"></div> |
||||
<div class="row justify-content-center font-bold text-xl"> |
||||
投組季報酬率 |
||||
</div> |
||||
<div class="mb-4" id="bar" style="max-height:60vh"></div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{# <div class="container mt-4"> |
||||
<form id="comment-form"> |
||||
<div class="form-group"> |
||||
<label for="username">輸入名稱:</label> |
||||
<input type="text" class="form-control" id="username" placeholder="Anonymous" value="Anonymous"> |
||||
</div> |
||||
<div class="form-group"> |
||||
<label for="comment">評論</label> |
||||
<textarea class="form-control" id="comment" rows="3"></textarea> |
||||
</div> |
||||
<button type="submit" class="btn btn-primary">Add Comment</button> |
||||
</form> |
||||
<div id="comments" class="mt-4"></div> |
||||
</div> #} |
||||
</div> |
||||
</div> |
||||
|
||||
{% endblock %} |
||||
|
||||
{% block script %} |
||||
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script> |
||||
<script type="text/javascript"> |
||||
const wlayout = { |
||||
'autosize': true, |
||||
'barmode': 'relative', |
||||
'title': {'text': ''}, |
||||
'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0], 'title':''}, |
||||
'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0], 'title':''}, |
||||
'margin': {'l': 50, 'r': 50, 't': 10, 'b': 50}, |
||||
'legend': {'yanchor': 'bottom', 'y': 1.3, 'xanchor': 'left', 'x': 0, 'orientation':'h', |
||||
'font': {'size':8} |
||||
} |
||||
}; |
||||
const rlayout = { |
||||
'autosize': true, |
||||
'title': {'text': ''}, |
||||
'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0], 'title':'', 'rangeslider': {'visible': true} |
||||
}, |
||||
'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0], 'title':''}, |
||||
'margin': {'l': 40, 'r': 20, 't': 30, 'b': 50}, |
||||
'legend': {'yanchor': 'top', 'y': 1.3, 'xanchor': 'left', 'x': 0.01, 'orientation':'h'} |
||||
}; |
||||
const blayout = { |
||||
'autosize': true, |
||||
'title': {'text': ''}, |
||||
'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0], 'title':''}, |
||||
'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0], 'title':''}, |
||||
'margin': {'l': 40, 'r': 20, 't': 50, 'b': 70}, |
||||
'legend': {'yanchor': 'top', 'y': 1.3, 'xanchor': 'left', 'x': 0.01, 'orientation':'h'} |
||||
}; |
||||
var w = {{ data.weight|safe }}; |
||||
var r = {{ data.ret|safe }}; |
||||
var b = {{ data.bar|safe }}; |
||||
Plotly.newPlot("weight", w.data, wlayout, {responsive: true}); |
||||
Plotly.newPlot("price", r.data, rlayout, {responsive: true}); |
||||
Plotly.newPlot("bar", b.data, blayout, {responsive: true}); |
||||
</script> |
||||
{% endblock script %} |
||||
@ -0,0 +1,259 @@ |
||||
{% 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 %} |
||||
div.input-group > * { |
||||
border-radius: 0px; |
||||
} |
||||
div.card{ |
||||
border-radius: 2px; |
||||
} |
||||
.card-header { |
||||
color: #000093; |
||||
} |
||||
.scroll { |
||||
max-height: 450px; |
||||
overflow-y: auto; |
||||
} |
||||
@keyframes cursor { |
||||
0% { |
||||
opacity: 0; |
||||
} |
||||
40% { |
||||
opacity: 0; |
||||
} |
||||
50% { |
||||
opacity: 1; |
||||
} |
||||
90% { |
||||
opacity: 1; |
||||
} |
||||
100% { |
||||
opacity: 0.9; |
||||
} |
||||
} |
||||
{% endblock style %} |
||||
|
||||
{% block content %} |
||||
<div class="container-fluid" style="background-color: #ffffff;min-height:95%;position:relative;"> |
||||
<div class="container-fluid py-2"> |
||||
<div class="alert alert-dark m-0" role="alert"> |
||||
<ul class="fa-ul"> |
||||
<li><span class="fa-li"><i class="fa-solid fa-scroll"></i></span>兩次建立投資組合時間需大於60秒。</li> |
||||
<li><span class="fa-li"><i class="fa-solid fa-scroll"></i></span>資產數量大於1檔才會開始建立投資組合。</li> |
||||
<li><span class="fa-li"><i class="fa-solid fa-scroll"></i></span>未輸入投資組合名稱則會由系統隨機生成。</li> |
||||
</ul> |
||||
|
||||
</div><!-- Button trigger modal --> |
||||
|
||||
<div class="modal fade" id="intro" tabindex="-1" data-bs-backdrop="static"> |
||||
<div class="modal-dialog modal-dialog-centered"> |
||||
<div class="modal-content"> |
||||
<div class="modal-header"> |
||||
<h5 class="card-title text-xl font-bold pt-2"> |
||||
{% if session.tw == 1 %}台股{% endif %}投資組合策略建立指南</h5> |
||||
</div> |
||||
<div class="modal-body"> |
||||
<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 class="badge bg-secondary">加入</span>。</li> |
||||
<li class="list-group-item"><span class="ps-2">按下 <button type="button" class="btn btn-outline-primary btn-sm" disabled>確認資產</button> 後查看資產價格動態圖表。</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">按下 <button type="button" class="btn btn-outline-danger btn-sm" disabled>確認建立</button> 並查看回傳訊息。</span></li> |
||||
</ol> |
||||
</div> |
||||
<div class="modal-footer"> |
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">了解 !</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="card my-2" style="border-radius: 7px;"> |
||||
<div class="card-header d-flex"> |
||||
<div class="py-2 font-bold text-xl"> |
||||
名稱與選擇資產 |
||||
</div> |
||||
<button type="button" class="btn btn-secondary ms-auto" data-bs-toggle="modal" data-bs-target="#intro"> |
||||
如何建立投資組合 ? |
||||
</button> |
||||
</div> |
||||
<div class="card-body"> |
||||
{% cache 300 %} |
||||
<label for="portName" class="form-label font-bold">輸入投資組合名稱: </label> |
||||
<input id="portName" name="portName" type="text" class="form-control mb-3" placeholder="請輸入投資組合名稱..." required> |
||||
<label for="competition" class="form-label font-bold">選擇所屬課程或競賽: </label> |
||||
<select id="competition" class="form-select mb-3" size="1"> |
||||
{% include 'competitions.html' %} |
||||
</select> |
||||
{% endcache %} |
||||
<label for="stockAll" class="form-label font-bold">選擇資產: </label> |
||||
<div class="d-flex"> |
||||
<input name="assetSelect" class="form-control" list="datalistOptions" id="stockAll" placeholder="輸入資產名稱..."> |
||||
<datalist id="datalistOptions"> |
||||
|
||||
</datalist> |
||||
<button class="btn btn-secondary btn-sm" |
||||
type="button" |
||||
id="addStockBtn"> |
||||
加入 |
||||
</button> |
||||
</div> |
||||
<div> |
||||
<ol class="list-group list-group-numbered py-3" id="stock-list" type="1"> |
||||
{% for s in stock %} |
||||
<li class="list-group-item"> |
||||
<span class="px-2">{{ s|safe }}</span> |
||||
<a class="btn btn-sm btn-danger float-right delete-btn"> |
||||
<i class="fas fa-trash-alt"></i> |
||||
</a> |
||||
</li> |
||||
{% endfor %} |
||||
</ol> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% cache 300 %} |
||||
<div class="card" style="border-radius: 7px;"> |
||||
<div class="card-header d-flex"> |
||||
<div class="py-2 font-bold text-lg"> |
||||
價格動態圖表 |
||||
</div> |
||||
<div class="btn-group ms-auto"> |
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="submit-btn"> |
||||
確認資產 |
||||
</button> |
||||
</div> |
||||
</div> |
||||
<div class="card-body"> |
||||
<div id="graph" style="max-height:50vh"> |
||||
<span> |
||||
圖表將在此渲染。 |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="card mt-3"> |
||||
<div class="d-flex p-2"> |
||||
<div class="p-2 font-bold text-lg"> |
||||
投組最佳化配置 |
||||
</div> |
||||
<div class="btn-group ms-auto"> |
||||
<button type="button p-0" class="btn btn-outline-danger btn-sm" id="submit-port"> |
||||
確認建立 |
||||
</button> |
||||
</div> |
||||
</div> |
||||
<div class="input-group"> |
||||
<span class="input-group-text bg-info">滾動視窗大小</span> |
||||
<select id="lookback" class="form-select"> |
||||
<option value="21">1個月</option> |
||||
<option value="63">3個月</option> |
||||
<option selected value="126">6個月</option> |
||||
<option value="252">12個月</option> |
||||
</select> |
||||
</div> |
||||
<div class="input-group"> |
||||
<span class="input-group-text bg-info">最佳化頻率</span> |
||||
<select id="opt-frequency" class="form-select"> |
||||
<option value="21">每月</option> |
||||
<option value="63">每季</option> |
||||
<option selected value="126">每半年</option> |
||||
<option value="252">每年</option> |
||||
</select> |
||||
</div> |
||||
<div class="input-group"> |
||||
<span class="input-group-text bg-info">最佳化目標函數</span> |
||||
<select id="role-select" class="form-select" onchange="changeFunc(value);"> |
||||
<option selected value="max_sharpe">最大化夏普比率</option> |
||||
<option value="max_sortino">最大化索提諾比率</option> |
||||
<option value="min_volatility">最小化波動率</option> |
||||
<option value="quadratic_utility">最大化效用函數</option> |
||||
</select> |
||||
</div> |
||||
<div class="input-group" style="display: none;" id="gamma"> |
||||
<span class="input-group-text bg-info">風險厭惡係數</span> |
||||
<input type="number" id="util-gamma" name="targetAnnualVolatility" class="form-control fmt-pct" value="30" autocomplete="off"> |
||||
<span class="input-group-text">%</span> |
||||
</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 py-2">確認建立投資組合</h5> |
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||
</div> |
||||
<div class="modal-body"> |
||||
<ol class="list-group list-group-flush list-group-numbered"> |
||||
<li class="list-group-item">確認後將會暫時關閉建立功能,請等待完成訊息,勿頻繁提交建立請求。</li> |
||||
<li class="list-group-item">兩次投資組合建立時間需大於60秒。</li> |
||||
</ol> |
||||
<div class="input-group mt-3"> |
||||
<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 id='modalTitle' 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"> |
||||
<div id="sucMes"> |
||||
<span>投資組合已開始建立,請1分鐘後至結果分析查詢。</span> |
||||
</div> |
||||
</span> |
||||
</div> |
||||
<div class="modal-footer"> |
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">確認</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endcache %} |
||||
</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> |
||||
<script> |
||||
var dlist = document.getElementById("datalistOptions"); |
||||
var str = ''; |
||||
{% if session.tw==0 %} |
||||
{% for key, data in data_us.items() -%} |
||||
str+='<option value="{{ key|e }}">{{ key|e }} | {{ data|e }}</option>'; |
||||
{% endfor %} |
||||
{% endif %} |
||||
{% for key, data in data_tw.items() -%} |
||||
str+='<option value="{{ key|e }}">{{ key|e }} | {{ data|e }} </option>'; |
||||
{% endfor %} |
||||
dlist.innerHTML = str; |
||||
</script> |
||||
{% endblock script %} |
||||