|
|
|
#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 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"
|
|
|
|
# }
|
|
|
|
|
|
|
|
|
|
|
|
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, ))
|
|
|
|
# print(curs.mogrify(sql, (stock_list,)))
|
|
|
|
data= curs.fetchall()
|
|
|
|
else:
|
|
|
|
sql1="SELECT ticker, date, price, return FROM stock_price where ticker = ANY(%s)"
|
|
|
|
sql2="SELECT ticker, date, price, return FROM stock_price_tw where ticker = ANY(%s) ;"
|
|
|
|
with conn:
|
|
|
|
with conn.cursor() as curs:
|
|
|
|
curs.execute(sql1, (stock_list,))
|
|
|
|
data_us= curs.fetchall()
|
|
|
|
curs.execute(sql2, (stock_list,))
|
|
|
|
data_tw= curs.fetchall()
|
|
|
|
data = data_us+data_tw
|
|
|
|
dfStock = pd.DataFrame(data, columns=['ticker', 'date', 'price', 'return'])
|
|
|
|
dfStock['date'] = pd.to_datetime(dfStock['date'])
|
|
|
|
dfStock = dfStock.drop_duplicates()
|
|
|
|
g = dfStock.groupby('ticker')
|
|
|
|
port = pd.concat([g.get_group(t).set_index('date')['price'] for t in stock_list], axis=1, join='inner')
|
|
|
|
port.columns=stock_list
|
|
|
|
return port
|
|
|
|
|
|
|
|
|
|
|
|
# Define the route for the index pages
|
|
|
|
@app.route('/')
|
|
|
|
def index():
|
|
|
|
return render_template('base.html')
|
|
|
|
|
|
|
|
# Login Page
|
|
|
|
@app.route('/login')
|
|
|
|
def login():
|
|
|
|
# for key in session:
|
|
|
|
# print(key, session[key])
|
|
|
|
# print(session.get('username'), session['username'], session.get('username') and session['username'])
|
|
|
|
return render_template('login.html')
|
|
|
|
@app.route('/login', methods=['POST'])
|
|
|
|
def login_post():
|
|
|
|
# Get the username and password from the form
|
|
|
|
username = request.form.get('username')
|
|
|
|
password = request.form.get('password')
|
|
|
|
print(username, password)
|
|
|
|
|
|
|
|
|
|
|
|
## Connect to the database
|
|
|
|
conn = psycopg2.connect(**SQL_CONFIG)
|
|
|
|
with conn:
|
|
|
|
with conn.cursor() as curs:
|
|
|
|
curs.execute("select * from users where username = %s;", (username, ))
|
|
|
|
data = curs.fetchone()
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
# Authentication
|
|
|
|
if (data is None) or (username is None) or (password is None):
|
|
|
|
flash('使用者代號不對或密碼不對,請再試一次。', 'danger')
|
|
|
|
return render_template('login.html')
|
|
|
|
elif check_password_hash(data[2], password):
|
|
|
|
session['username'] = username
|
|
|
|
session['user_id'] = data[0]
|
|
|
|
session['privilege'] = data[-1]
|
|
|
|
session['update_freq'] = 100
|
|
|
|
session['lastCreateTime'] = time.time()
|
|
|
|
session['tw'] = 1
|
|
|
|
return redirect(url_for('index'))
|
|
|
|
else:
|
|
|
|
flash('使用者代號不對或密碼不對,請再試一次。', 'danger')
|
|
|
|
return render_template('login.html')
|
|
|
|
|
|
|
|
# Registration Page
|
|
|
|
@app.route('/registration')
|
|
|
|
def registration():
|
|
|
|
if login_required():
|
|
|
|
return redirect(url_for('index'))
|
|
|
|
return render_template('registration.html')
|
|
|
|
@app.route('/registration', methods=['POST'])
|
|
|
|
def registration_post():
|
|
|
|
# Get the username and password from the form
|
|
|
|
username = request.form.get('username')
|
|
|
|
password = request.form.get('password')
|
|
|
|
rep_password = request.form.get('rep-password')
|
|
|
|
# check password
|
|
|
|
if not password is None and password == rep_password:
|
|
|
|
print(username, password)
|
|
|
|
conn = psycopg2.connect(**SQL_CONFIG)
|
|
|
|
## Connect to the database
|
|
|
|
with conn.cursor() as curs:
|
|
|
|
curs.execute("select * from users where username = %s;", (username, ))
|
|
|
|
data = curs.fetchone()
|
|
|
|
if data is None:
|
|
|
|
with conn:
|
|
|
|
with conn.cursor() as curs:
|
|
|
|
curs.execute("insert into users (username, password) values (%s, %s);", (username, generate_password_hash(password)))
|
|
|
|
# conn.commit()
|
|
|
|
else:
|
|
|
|
flash('使用者已存在。', 'warning')
|
|
|
|
return redirect(url_for('login'))
|
|
|
|
conn.close()
|
|
|
|
name = username.split('@')[0]
|
|
|
|
flash(f'註冊成功! 歡迎您, {name}。', 'success')
|
|
|
|
return redirect(url_for('login'))
|
|
|
|
else:
|
|
|
|
flash('密碼不符合,請再次輸入。', 'warning')
|
|
|
|
return redirect(url_for('registration'))
|
|
|
|
|
|
|
|
# Logout Page
|
|
|
|
@app.route('/logout', methods=['GET'])
|
|
|
|
def logout():
|
|
|
|
if login_required():
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
flash('請先登入。', 'warning')
|
|
|
|
return redirect(url_for('login'))
|
|
|
|
if 'username' in session:
|
|
|
|
# for key in list(session.keys()):
|
|
|
|
# print(key, session[key])
|
|
|
|
# for key in list(session.keys()):
|
|
|
|
# session.pop(key)
|
|
|
|
session.clear()
|
|
|
|
return redirect(url_for('index'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/strategy')
|
|
|
|
def strategy():
|
|
|
|
if login_required():
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
flash('使用投組功能請先登入。', 'warning')
|
|
|
|
return redirect(url_for('login'))
|
|
|
|
session['tw'] = 0
|
|
|
|
return render_template('strategy_tw.html', data_us = data_us, data_tw=data_tw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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) > 750:
|
|
|
|
port = port.iloc[-750:, :]
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
fig = port.plot(title='資產價格走勢', labels=dict(index="Date", value="Price", variable="Assets"))
|
|
|
|
fig['layout'] = {}
|
|
|
|
print(type(stock_list))
|
|
|
|
|
|
|
|
# 序列化
|
|
|
|
graphJSON = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
|
|
|
|
|
|
|
|
|
|
|
|
for key in request.form:
|
|
|
|
print(key, request.form[key])
|
|
|
|
# Do something with the stock list heres
|
|
|
|
return graphJSON
|
|
|
|
# return jsonify({'message': 'Stock list received successfully, NOOOOO'})
|
|
|
|
|
|
|
|
@app.route('/postPort', methods=['POST'])
|
|
|
|
def buildPort():
|
|
|
|
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'] < 10:
|
|
|
|
print("UNTIL: ", time.time()-session['lastCreateTime'])
|
|
|
|
return '''<span>投資組合建立時間間隔(或與登入時間間隔)必須大於60秒!</span>'''
|
|
|
|
print('last_creation', time.time() - session['lastCreateTime'])
|
|
|
|
session['lastCreateTime'] = time.time()
|
|
|
|
print('last_creation', session['lastCreateTime'])
|
|
|
|
print("-"*10)
|
|
|
|
for key in request.form:
|
|
|
|
print(key, request.form[key], type(request.form[key]))
|
|
|
|
|
|
|
|
# Portfolio Info
|
|
|
|
name = request.form.get('name')
|
|
|
|
if name == '':
|
|
|
|
prefix=''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
|
|
|
name= prefix + f"-{round(time.time()%100, 2)}"
|
|
|
|
comp = request.form.get('comp')
|
|
|
|
ts = int(request.form.get('ts'))
|
|
|
|
ts = datetime.fromtimestamp(ts/1000)
|
|
|
|
role = request.form.get('role')
|
|
|
|
ratio = float(request.form.get('ratio'))
|
|
|
|
comment = request.form.get('comment')
|
|
|
|
stock_list = json.loads(request.form.get('stockList'))
|
|
|
|
|
|
|
|
# Algorithm MVO
|
|
|
|
print("-"*10)
|
|
|
|
print("Enter Algorithms")
|
|
|
|
print("-"*10)
|
|
|
|
# time.sleep(20)
|
|
|
|
# Query DB
|
|
|
|
market_asset = '0050.TW' if session['tw']==1 else 'SPY'
|
|
|
|
conn = psycopg2.connect(**SQL_CONFIG)
|
|
|
|
if market_asset in stock_list:
|
|
|
|
port = get_stock(conn, stock_list, session['tw'])
|
|
|
|
market = port[market_asset]
|
|
|
|
else:
|
|
|
|
port = get_stock(conn, stock_list+[market_asset], session['tw'])
|
|
|
|
market = port[market_asset]
|
|
|
|
port = port[stock_list]
|
|
|
|
|
|
|
|
length, num = port.shape
|
|
|
|
tsize = int(length*ratio)
|
|
|
|
# time label
|
|
|
|
train_label = port.index[1:][:tsize]
|
|
|
|
test_label = port.index[1:][tsize:]
|
|
|
|
|
|
|
|
# data
|
|
|
|
data_return = port.pct_change().dropna().to_numpy()
|
|
|
|
market_return = market.pct_change().dropna().to_numpy()
|
|
|
|
train = data_return[:tsize, :]
|
|
|
|
test = data_return[tsize:, :]
|
|
|
|
train_market = market_return[:tsize]
|
|
|
|
test_market = market_return[tsize:]
|
|
|
|
|
|
|
|
# optimization
|
|
|
|
sol = MVO.opt(train, role=role)
|
|
|
|
train_info = MVO.portfolio_info(sol, train, train_market)
|
|
|
|
test_info = MVO.portfolio_info(sol, test, test_market)
|
|
|
|
# print(sol, train_info, test_info)
|
|
|
|
# print("-"*10)
|
|
|
|
# print(ts, name, session.get('username'), comp,
|
|
|
|
# role, test_info['annual_ret'], test_info['vol'], test_info['mdd'], test_info['annual_sr'],
|
|
|
|
# test_info['beta'], test_info['alpha'], test_info['var10'], test_info['R2'], True, comment, stock_list, list(sol), sep=", ")
|
|
|
|
# print("-"*10)
|
|
|
|
data = (ts, name, session.get('username').split('@')[0], comp, role, ratio, test_info['annual_ret'],
|
|
|
|
test_info['vol'], test_info['mdd'], test_info['annual_sr'],
|
|
|
|
test_info['beta'], test_info['alpha'], test_info['var10'], test_info['R2'], True, comment, stock_list, list(sol))
|
|
|
|
sql='insert into strategy \
|
|
|
|
(date, name, username, competition, role, ratio, annual_ret, vol, mdd, annual_sr, beta, alpha, var10, R2, tw, comment, assets, assets_position)\
|
|
|
|
values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING id;'
|
|
|
|
with conn:
|
|
|
|
with conn.cursor() as curs:
|
|
|
|
curs.execute(sql, data)
|
|
|
|
strategy_id = curs.fetchone()[0]
|
|
|
|
conn.close()
|
|
|
|
print("\n------Write in Success--------\n")
|
|
|
|
return f'''<span>投資組合已完成建立,請至 <a class="badge rounded-pill text-bg-primary" href="/result_view?strategy_id={strategy_id}"><span class="badge bg-success">{strategy_id}</span></a>查詢分析結果。</span>'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/custom')
|
|
|
|
def custom():
|
|
|
|
if login_required():
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
flash('使用投組功能請先登入。', 'warning')
|
|
|
|
return redirect(url_for('login'))
|
|
|
|
return render_template('custom.html', message='No')
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/result')
|
|
|
|
def result():
|
|
|
|
if login_required():
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
flash('使用投組功能請先登入。', 'warning')
|
|
|
|
return redirect(url_for('login'))
|
|
|
|
|
|
|
|
sql="""select id, date, name, username, annual_ret, vol, annual_sr\
|
|
|
|
from strategy order by id desc limit 100;"""
|
|
|
|
conn = psycopg2.connect(**SQL_CONFIG)
|
|
|
|
with conn:
|
|
|
|
with conn.cursor() as curs:
|
|
|
|
curs.execute(sql)
|
|
|
|
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'))
|
|
|
|
strategy_id = request.args.get('strategy_id')
|
|
|
|
print(strategy_id)
|
|
|
|
return render_template('result_view.html')
|
|
|
|
|
|
|
|
# handle login failed
|
|
|
|
# @app.errorhandler(401)
|
|
|
|
# def page_not_found(e):
|
|
|
|
# return response('<p>Failed</p>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
app.run(host='0.0.0.0', port=8000)
|