diff --git a/cool.json b/cool.json
deleted file mode 100644
index 0051421..0000000
--- a/cool.json
+++ /dev/null
@@ -1 +0,0 @@
-"{\"AAPL\":{\"1591142400000\":0.03,\"1593648000000\":0.21,\"1596412800000\":0.4,\"1598918400000\":0.4,\"1601510400000\":0.27,\"1604016000000\":0.13,\"1606780800000\":0.0,\"1609372800000\":0.03,\"1612224000000\":0.0,\"1614816000000\":0.0,\"1617580800000\":0.0,\"1620086400000\":0.0,\"1622678400000\":0.0,\"1625184000000\":0.18,\"1627948800000\":0.2,\"1630454400000\":0.21,\"1633046400000\":0.0,\"1635724800000\":0.0,\"1638316800000\":0.4,\"1640908800000\":0.25,\"1643673600000\":0.3,\"1646265600000\":0.0,\"1648771200000\":0.0,\"1651536000000\":0.02,\"1654128000000\":0.0,\"1656979200000\":0.0,\"1659484800000\":0.0,\"1661990400000\":0.21,\"1664755200000\":0.02,\"1667260800000\":0.0,\"1669852800000\":0.0,\"1672704000000\":0.0,\"1675296000000\":0.0},\"A\":{\"1591142400000\":0.13,\"1593648000000\":0.0,\"1596412800000\":0.0,\"1598918400000\":0.0,\"1601510400000\":0.13,\"1604016000000\":0.12,\"1606780800000\":0.31,\"1609372800000\":0.05,\"1612224000000\":0.0,\"1614816000000\":0.0,\"1617580800000\":0.0,\"1620086400000\":0.0,\"1622678400000\":0.22,\"1625184000000\":0.45,\"1627948800000\":0.41,\"1630454400000\":0.34,\"1633046400000\":0.19,\"1635724800000\":0.0,\"1638316800000\":0.0,\"1640908800000\":0.0,\"1643673600000\":0.0,\"1646265600000\":0.0,\"1648771200000\":0.0,\"1651536000000\":0.0,\"1654128000000\":0.0,\"1656979200000\":0.25,\"1659484800000\":0.49,\"1661990400000\":0.2,\"1664755200000\":0.13,\"1667260800000\":0.27,\"1669852800000\":0.31,\"1672704000000\":0.29,\"1675296000000\":0.43},\"ABBV\":{\"1591142400000\":0.0,\"1593648000000\":0.0,\"1596412800000\":0.0,\"1598918400000\":0.0,\"1601510400000\":0.0,\"1604016000000\":0.0,\"1606780800000\":0.0,\"1609372800000\":0.13,\"1612224000000\":0.16,\"1614816000000\":0.0,\"1617580800000\":0.0,\"1620086400000\":0.0,\"1622678400000\":0.0,\"1625184000000\":0.0,\"1627948800000\":0.0,\"1630454400000\":0.0,\"1633046400000\":0.0,\"1635724800000\":0.0,\"1638316800000\":0.0,\"1640908800000\":0.3,\"1643673600000\":0.38,\"1646265600000\":0.6,\"1648771200000\":0.6,\"1651536000000\":0.6,\"1654128000000\":0.47,\"1656979200000\":0.6,\"1659484800000\":0.0,\"1661990400000\":0.0,\"1664755200000\":0.0,\"1667260800000\":0.3,\"1669852800000\":0.35,\"1672704000000\":0.3,\"1675296000000\":0.0},\"AFL\":{\"1591142400000\":0.0,\"1593648000000\":0.0,\"1596412800000\":0.0,\"1598918400000\":0.0,\"1601510400000\":0.0,\"1604016000000\":0.0,\"1606780800000\":0.55,\"1609372800000\":0.13,\"1612224000000\":0.24,\"1614816000000\":0.3,\"1617580800000\":0.43,\"1620086400000\":0.43,\"1622678400000\":0.4,\"1625184000000\":0.0,\"1627948800000\":0.0,\"1630454400000\":0.0,\"1633046400000\":0.0,\"1635724800000\":0.0,\"1638316800000\":0.0,\"1640908800000\":0.0,\"1643673600000\":0.33,\"1646265600000\":0.4,\"1648771200000\":0.34,\"1651536000000\":0.1,\"1654128000000\":0.53,\"1656979200000\":0.15,\"1659484800000\":0.23,\"1661990400000\":0.11,\"1664755200000\":0.13,\"1667260800000\":0.44,\"1669852800000\":0.34,\"1672704000000\":0.41,\"1675296000000\":0.57},\"TSLA\":{\"1591142400000\":0.23,\"1593648000000\":0.6,\"1596412800000\":0.6,\"1598918400000\":0.6,\"1601510400000\":0.6,\"1604016000000\":0.6,\"1606780800000\":0.15,\"1609372800000\":0.6,\"1612224000000\":0.6,\"1614816000000\":0.34,\"1617580800000\":0.0,\"1620086400000\":0.0,\"1622678400000\":0.0,\"1625184000000\":0.0,\"1627948800000\":0.0,\"1630454400000\":0.23,\"1633046400000\":0.53,\"1635724800000\":0.6,\"1638316800000\":0.6,\"1640908800000\":0.45,\"1643673600000\":0.0,\"1646265600000\":0.0,\"1648771200000\":0.06,\"1651536000000\":0.2,\"1654128000000\":0.0,\"1656979200000\":0.0,\"1659484800000\":0.0,\"1661990400000\":0.33,\"1664755200000\":0.6,\"1667260800000\":0.0,\"1669852800000\":0.0,\"1672704000000\":0.0,\"1675296000000\":0.0},\"AMZN\":{\"1591142400000\":0.6,\"1593648000000\":0.19,\"1596412800000\":0.0,\"1598918400000\":0.0,\"1601510400000\":0.0,\"1604016000000\":0.02,\"1606780800000\":0.0,\"1609372800000\":0.0,\"1612224000000\":0.0,\"1614816000000\":0.0,\"1617580800000\":0.0,\"1620086400000\":0.0,\"1622678400000\":0.0,\"1625184000000\":0.0,\"1627948800000\":0.0,\"1630454400000\":0.0,\"1633046400000\":0.0,\"1635724800000\":0.0,\"1638316800000\":0.0,\"1640908800000\":0.0,\"1643673600000\":0.0,\"1646265600000\":0.0,\"1648771200000\":0.0,\"1651536000000\":0.0,\"1654128000000\":0.0,\"1656979200000\":0.0,\"1659484800000\":0.28,\"1661990400000\":0.15,\"1664755200000\":0.12,\"1667260800000\":0.0,\"1669852800000\":0.0,\"1672704000000\":0.0,\"1675296000000\":0.0},\"GOOGL\":{\"1591142400000\":0.0,\"1593648000000\":0.0,\"1596412800000\":0.0,\"1598918400000\":0.0,\"1601510400000\":0.0,\"1604016000000\":0.08,\"1606780800000\":0.0,\"1609372800000\":0.07,\"1612224000000\":0.0,\"1614816000000\":0.35,\"1617580800000\":0.57,\"1620086400000\":0.57,\"1622678400000\":0.38,\"1625184000000\":0.37,\"1627948800000\":0.39,\"1630454400000\":0.22,\"1633046400000\":0.28,\"1635724800000\":0.4,\"1638316800000\":0.0,\"1640908800000\":0.0,\"1643673600000\":0.0,\"1646265600000\":0.0,\"1648771200000\":0.0,\"1651536000000\":0.0,\"1654128000000\":0.0,\"1656979200000\":0.0,\"1659484800000\":0.0,\"1661990400000\":0.0,\"1664755200000\":0.0,\"1667260800000\":0.0,\"1669852800000\":0.0,\"1672704000000\":0.0,\"1675296000000\":0.0},\"SPY\":{\"1591142400000\":0.0,\"1593648000000\":0.0,\"1596412800000\":0.0,\"1598918400000\":0.0,\"1601510400000\":0.0,\"1604016000000\":0.04,\"1606780800000\":0.0,\"1609372800000\":0.0,\"1612224000000\":0.0,\"1614816000000\":0.0,\"1617580800000\":0.0,\"1620086400000\":0.0,\"1622678400000\":0.0,\"1625184000000\":0.0,\"1627948800000\":0.0,\"1630454400000\":0.0,\"1633046400000\":0.0,\"1635724800000\":0.0,\"1638316800000\":0.0,\"1640908800000\":0.0,\"1643673600000\":0.0,\"1646265600000\":0.0,\"1648771200000\":0.0,\"1651536000000\":0.08,\"1654128000000\":0.0,\"1656979200000\":0.0,\"1659484800000\":0.0,\"1661990400000\":0.0,\"1664755200000\":0.0,\"1667260800000\":0.0,\"1669852800000\":0.0,\"1672704000000\":0.0,\"1675296000000\":0.0}}"
\ No newline at end of file
diff --git a/main.py b/main.py
index df29234..5e30042 100644
--- a/main.py
+++ b/main.py
@@ -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 '''投資組合建立時間間隔(或與登入時間間隔)必須大於60秒!'''
- 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'''投資組合無法建立,資料長度與所選參數不符。'''
+ 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'''投資組合已完成建立,請至 {strategy_id}查詢分析結果。'''
+ return f'''投資組合已完成建立,請點擊 {strategy_id}查詢分析結果。'''
@@ -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)
diff --git a/sql_script/create_strategy.sql b/sql_script/create_strategy.sql
index 24b7d4c..f3918ab 100644
--- a/sql_script/create_strategy.sql
+++ b/sql_script/create_strategy.sql
@@ -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);
diff --git a/static/js/addStock.js b/static/js/addStock.js
index 74246b7..c809e63 100644
--- a/static/js/addStock.js
+++ b/static/js/addStock.js
@@ -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');
diff --git a/templates/base.html b/templates/base.html
index be35d30..93a82a2 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -23,11 +23,10 @@
{% endblock %}