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 %}