|
|
|
@ -13,6 +13,7 @@ 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 |
|
|
|
@ -20,32 +21,32 @@ pd.options.plotting.backend = "plotly" |
|
|
|
|
|
|
|
|
|
# PARAMETERS |
|
|
|
|
CONFIGS = { |
|
|
|
|
"ENV": "development", |
|
|
|
|
"DEBUG": True, |
|
|
|
|
# "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" |
|
|
|
|
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_volatilty='最小化波動率', quadratic_utility='最大化效用函數') |
|
|
|
|
|
|
|
|
|
app = Flask(__name__) |
|
|
|
|
app.config.from_mapping(CONFIGS) |
|
|
|
@ -68,7 +69,6 @@ def get_stock(conn, stock_list, tw): |
|
|
|
|
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)" |
|
|
|
@ -87,6 +87,27 @@ def get_stock(conn, stock_list, tw): |
|
|
|
|
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 |
|
|
|
@ -97,9 +118,6 @@ def index(): |
|
|
|
|
# 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(): |
|
|
|
@ -147,7 +165,6 @@ def registration_post(): |
|
|
|
|
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: |
|
|
|
@ -240,7 +257,6 @@ def submit_stock_list(): |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
@ -264,10 +280,10 @@ def buildPort(): |
|
|
|
|
if time.time() - session['lastCreateTime'] < 10: |
|
|
|
|
print("UNTIL: ", time.time()-session['lastCreateTime']) |
|
|
|
|
return '''<span>投資組合建立時間間隔(或與登入時間間隔)必須大於60秒!</span>''' |
|
|
|
|
print('last_creation', time.time() - session['lastCreateTime']) |
|
|
|
|
# print('last_creation', time.time() - session['lastCreateTime']) |
|
|
|
|
session['lastCreateTime'] = time.time() |
|
|
|
|
print('last_creation', session['lastCreateTime']) |
|
|
|
|
print("-"*10) |
|
|
|
|
# print('last_creation', session['lastCreateTime']) |
|
|
|
|
# print("-"*10) |
|
|
|
|
for key in request.form: |
|
|
|
|
print(key, request.form[key], type(request.form[key])) |
|
|
|
|
|
|
|
|
@ -276,22 +292,25 @@ def buildPort(): |
|
|
|
|
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 = int(request.form.get('ts')) |
|
|
|
|
ts = datetime.fromtimestamp(ts/1000) |
|
|
|
|
# ts = int(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')) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ratio=0.7 |
|
|
|
|
|
|
|
|
|
# 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) |
|
|
|
@ -302,36 +321,32 @@ def buildPort(): |
|
|
|
|
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 > 757+lookback: |
|
|
|
|
port = port.iloc[-(757+lookback):, :] |
|
|
|
|
market = market.iloc[-757:] |
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)) |
|
|
|
|
# Get portfolio info. |
|
|
|
|
info = MVO.portfolio_info(np.array([1]), rets['Portfolio'].to_numpy().reshape(-1, 1), rets['Equally'].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'], True, comment, stock_list, json.dumps(weight.to_dict()), json.dumps(rets.to_dict())) |
|
|
|
|
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)\ |
|
|
|
|
(date, name, username, competition, role, annual_ret, vol, mdd, annual_sr, beta, alpha, var10, R2, tw, notes, assets, weight, ret)\ |
|
|
|
|
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: |
|
|
|
@ -339,7 +354,7 @@ def buildPort(): |
|
|
|
|
strategy_id = curs.fetchone()[0] |
|
|
|
|
conn.close() |
|
|
|
|
print("\n------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>''' |
|
|
|
|
return f'''<span>投資組合已完成建立,請點擊 <a class="badge rounded-pill text-bg-warning" href="/result_view?strategy_id={strategy_id}">{strategy_id}</a>查詢分析結果。</span>''' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -361,8 +376,8 @@ def result(): |
|
|
|
|
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;""" |
|
|
|
|
sql="""select id, date, name, username, annual_ret, vol, annual_sr, mdd\ |
|
|
|
|
from strategy order by id desc limit 50;""" |
|
|
|
|
conn = psycopg2.connect(**SQL_CONFIG) |
|
|
|
|
with conn: |
|
|
|
|
with conn.cursor() as curs: |
|
|
|
@ -380,9 +395,38 @@ def result_view(): |
|
|
|
|
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') |
|
|
|
|
print(strategy_id) |
|
|
|
|
return render_template('result_view.html') |
|
|
|
|
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().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) |
|
|
|
|
return render_template('result_view.html', data=data) |
|
|
|
|
|
|
|
|
|
# handle login failed |
|
|
|
|
# @app.errorhandler(401) |
|
|
|
|