master
SeanChenTaipei 2 years ago
parent e87b7bafb2
commit 080cb86a2c
  1. 1
      cool.json
  2. 164
      main.py
  3. 16
      sql_script/create_strategy.sql
  4. 14
      static/js/addStock.js
  5. 7
      templates/base.html
  6. 2
      templates/login.html
  7. 4
      templates/result.html
  8. 152
      templates/result_view.html
  9. 14
      templates/strategy_tw.html

File diff suppressed because one or more lines are too long

@ -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)

@ -1,12 +1,11 @@
DROP TABLE IF EXISTS strategy;
CREATE TABLE strategy (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
name VARCHAR(64) NOT NULL,
username VARCHAR(64) NOT NULL,
competition VARCHAR(64) NOT NULL,
date VARCHAR(24) NOT NULL,
name VARCHAR(32) NOT NULL,
username VARCHAR(32) NOT NULL,
competition VARCHAR(32) NOT NULL,
role VARCHAR(20) NOT NULL,
ratio REAL NOT NULL,
annual_ret REAL NOT NULL,
vol REAL NOT NULL,
mdd REAL NOT NULL,
@ -16,10 +15,11 @@ CREATE TABLE strategy (
var10 REAL NOT NULL,
R2 REAL NOT NULL,
tw BOOLEAN DEFAULT TRUE,
comment VARCHAR(255),
notes VARCHAR(255),
assets TEXT[] NOT NULL,
assets_position REAL[] NOT NULL,
notes TEXT[][]
weight JSON NOT NULL,
ret JSON NOT NULL,
comments TEXT[][]
);
CREATE INDEX idx_user ON strategy (username);

@ -2,17 +2,11 @@
let stockList = ['2330.TW'];
let currentList = [];
const layout={'autosize': true, 'markers':true,
'title': {'text': 'Assets'},
'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0],
'rangeslider': {'visible': true},
'rangeselector':{'rangeselector':
{'buttons': [
{'count': 1, 'label': '1m', 'step': 'month', 'stepmode': 'backward'},
{'count': 6, 'label': '6m', 'step': 'month', 'stepmode': 'backward'},
{'count': 1, 'label': 'YTD', 'step': 'year', 'stepmode': 'todate'},
{'count': 1, 'label': '1y', 'step': 'year', 'stepmode': 'backward'}, {'step': 'all'}]}, 'rangeslider': {'visible': true},'type': 'date'}},
'title': {'text': ''},
'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0], 'rangeslider': {'visible': true}},
'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0], 'fixedrange': false},
'legend': {'yanchor': 'top', 'y': 1.8, 'xanchor': 'left', 'x': 0.01}, 'margin': {'l': 25, 'r': 5, 't': 10, 'b': 5},
'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');

@ -23,11 +23,10 @@
{% endblock %}
<style>
body {
// padding-top: 60px;
padding-bottom: 10px;
// padding-bottom: 10px;
font-family: Georgia, Arial, Geneva, Helvetica, serif !important;
background-color: #eee;
height:100vh;
height:95vh;
}
.navbar {
background-image: linear-gradient(to bottom right, #5d9faa , #c4e0e5);
@ -62,7 +61,7 @@
('/', 'index', '首頁', 'bi bi-house-fill'),
('/strategy', 'strategy', '建立策略', 'fa-solid fa-chart-pie'),
('/strategy_tw', 'strategy_tw', '台股建立策略', 'fa-solid fa-chart-pie'),
('/strategy_bl', 'strategy_bl', 'Black-Litterman配置', 'fa-solid fa-chess-knight'),
('/', 'strategy_bl', 'Black-Litterman配置', 'fa-solid fa-chess-knight'),
('/custom', 'custom', '自訂數據建立策略', 'bi bi-database-fill-add'),
('/result', 'result', '分析結果排行', 'fa-solid fa-chart-simple'),
('mailto:r10246002@ntu.edu.tw', 'error', '錯誤回報', 'bi bi-bug-fill')

@ -14,7 +14,7 @@
{% endblock %}
{% block content %}
<div class="container-fluid" style="background-color: #eee;min-height:100%;position:relative;">
<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">

@ -7,7 +7,7 @@
{% block title %}Result Page{% endblock%}
{% block content %}
<div class="container-fluid" style="background-color: #eee;">
<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-secondary p-3 mx-3" role="alert">
<div class="flex-row">
@ -67,6 +67,7 @@
<th scope="col">報酬率</th>
<th scope="col">夏普率</th>
<th scope="col">波動率</th>
<th scope="col">最大回落</th>
<th scope="col">創建時間</th>
</tr>
</thead>
@ -83,6 +84,7 @@
<td>{{ info[4] }}</td>
<td>{{ info[6] }}</td>
<td>{{ info[5] }}</td>
<td>{{ info[7] }}</td>
<td>{{ info[1] }}</td>
</tr>
</thead>

@ -13,20 +13,160 @@
{% endblock %}
{% block content %}
<div class="container-fluid" style="background-color: #eee;min-height:100%;position:relative;">
<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">
yeah
<div class="alert alert-light text-dark" role="alert">
<div class="container">
<div class="row">
<div class="col-3">
<strong>策略編號</strong>
</div>
<div class="col-6">
{{ data.id|safe }}
</div>
</div>
<hr>
<div class="row">
<div class="col-3">
<strong>策略名稱</strong>
</div>
<div class="col-6">
{{ data.name|safe }}
</div>
</div>
<hr>
<div class="row">
<div class="col-3">
<strong>建立者</strong>
</div>
<div class="col-6">
{{ data.username|safe }}
</div>
</div>
<hr>
<div class="row">
<div class="col-3">
<strong>建立時間</strong>
</div>
<div class="col-6">
{{ data.date|safe }}
</div>
</div>
<hr>
<div class="row">
<div class="col-3">
<strong>策略目標</strong>
</div>
<div class="col-6">
{{ data.role|safe }}
</div>
</div>
<div class="row">
<button class="btn btn-info 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 p-3 m-3">
<div class="p-3 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">R^2</th>
</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>
</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="" id="weight" style="max-height:30vh"></div>
<div class="row justify-content-center font-bold text-xl">
投組價值走勢
</div>
<div class="" id="price" style="max-height:30vh"></div>
<div class="row justify-content-center font-bold text-xl">
投組季報酬率
</div>
<div class="" id="bar" style="max-height:30vh"></div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}
{% block script %}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://code.highcharts.com/highcharts.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': 50, 'b': 50},
'legend': {'yanchor': 'top', 'y': 1.2, 'xanchor': 'left', 'x': 0.01, 'orientation':'h'}
};
const rlayout = {
'autosize': true,
'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': 50, 'b': 50},
'legend': {'yanchor': 'top', 'y': 1.2, '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': 50, 'r': 50, 't': 50, 'b': 50},
'legend': {'yanchor': 'top', 'y': 1.2, '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 %}

@ -123,7 +123,7 @@ div.card{
</div>
<div class="card-body">
<div id="graph" style="max-height:60vh">
<div id="graph" style="max-height:50vh">
<span>
按下
<button type="button" class="btn btn-outline-primary btn-sm" disabled>確認資產</button>
@ -148,10 +148,10 @@ div.card{
<div class="input-group">
<span class="input-group-text bg-info">輸入數據時長</span>
<select id="lookback" class="form-select">
<option value="21"></option>
<option value="63">每季</option>
<option selected value="126">每半年</option>
<option value="252">每年</option>
<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">
@ -169,9 +169,7 @@ div.card{
<option selected value="max_sharpe">最大化夏普比率</option>
<option value="max_sortino">最大化索提諾比率</option>
<option value="min_volatility">最小化波動率</option>
<option value="quadratic_utility">
最大化效用函數
</option>
<option value="quadratic_utility">最大化效用函數</option>
</select>
</div>
<div class="input-group" style="display: none;" id="gamma">

Loading…
Cancel
Save