You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

541 lines
19 KiB

#coding=utf-8
from flask import Flask, render_template, request, redirect, url_for, g, session, flash, jsonify
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 psycopg2.extras
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 = dict(
database= os.getenv("PGDATABASE"),
user=os.getenv("PGUSER"),
host=os.getenv("PGHOST"),
port=os.getenv("PGPORT"),
password=os.getenv("PGPASSWORD")
)
# SQL_CONFIG = dict(
# database="railway",
# user="postgres",
# host="containers-us-west-103.railway.app",
# port="5913",
# password="gv5Mh7cPjCm9YTjAmsYD"
# )
# SQL_CONFIG = {
# 'database': "tpm",
# 'user': "hsienchen",
# 'host': "127.0.0.1",
# 'port': "5432"
# }
role_map = dict(max_sharpe='最大化夏普比率', max_sortino='最大化索提諾比率', min_volatility='最小化波動率', quadratic_utility='最大化效用函數')
app = Flask(__name__)
app.config.from_mapping(CONFIGS)
# 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, return 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, 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
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('/')
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'] = []
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()
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
return render_template('strategy_tw.html', data_us = data_us, data_tw=data_tw, stock=['TSLA'])
@app.route('/strategy_tw')
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
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'])
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:
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("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("-"*10)
print("Enter Algorithms")
print("-"*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()), json.dumps(rets.to_dict()))
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:
curs.execute(sql, data)
strategy_id = curs.fetchone()[0]
conn.close()
print("\n------Strategy write in Success--------\n")
return f'''<span>投資組合已完成建立,請點擊 <a class="badge rounded-pill text-bg-warning" href="/result_view?strategy_id={strategy_id}">{strategy_id}</a>查詢分析結果。</span>'''
@app.route('/custom')
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)
@app.route('/result', methods=['GET', 'POST'])
def result():
if login_required():
pass
else:
flash('使用投組功能請先登入。', 'warning')
return redirect(url_for('login'))
if request.method=='GET':
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 render_template('result.html', strategy_data=data)
elif request.method=='POST':
role = request.form.get('role')
comp = request.form.get('competition')
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 render_template('result.html', strategy_data=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
print("result", role)
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 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 = pd.DataFrame(data['weight'])
r = pd.DataFrame(data['ret'])
# 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='0.0.0.0')