SeanChenTaipei 2 years ago
parent 8b34c70a0d
commit 6977b88ac2
  1. 66
      main.py
  2. BIN
      static/img/file.jpg
  3. BIN
      static/img/file.png
  4. 2
      static/js/addStock.js
  5. 88
      templates/custom.html
  6. 6
      templates/result_view.html

@ -118,6 +118,8 @@ def index():
# Login Page
@app.route('/login')
def login():
if 'username' in session:
return render_template('base.html')
return render_template('login.html')
@app.route('/login', methods=['POST'])
def login_post():
@ -325,9 +327,9 @@ def buildPort():
n = len(port.index)
if n < lookback+backtest+63:
return f'''<span>投資組合無法建立,資料長度與所選參數不符。</span>'''
elif n > 909+lookback:
port = port.iloc[-(909+lookback):, :]
market = market.iloc[-909:]
elif n > 1009+lookback:
port = port.iloc[-(1009+lookback):, :]
market = market.iloc[-1009:]
else:
market = market.iloc[lookback:]
@ -341,7 +343,7 @@ def buildPort():
# Get portfolio info.
info = MVO.portfolio_info(np.array([1]), rets['Portfolio'].to_numpy().reshape(-1, 1), rets['Equally'].to_numpy())
info = MVO.portfolio_info(np.array([1]), rets['Portfolio'].to_numpy().reshape(-1, 1), market.pct_change().dropna().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()))
@ -365,7 +367,61 @@ def custom():
else:
flash('使用投組功能請先登入。', 'warning')
return redirect(url_for('login'))
return render_template('custom.html', message='No')
return render_template('custom.html')
@app.route('/custom', methods=['POST'])
def custom_post():
if login_required():
pass
else:
flash('使用投組功能請先登入。', 'warning')
return redirect(url_for('login'))
port = pd.read_csv(request.files['csv_file'], index_col=0, parse_dates=True)
role = request.form.get('role')
lookback = int(request.form.get('lookback'))
backtest = int(request.form.get('frequency'))
gamma = float(request.form.get('gamma'))/100
# Optimization
n = len(port.index)
if n < lookback+backtest+63:
return f'''<span>投資組合無法建立,資料長度與所選參數不符。</span>'''
elif n > 1009+lookback:
port = port.iloc[-(1009+lookback):, :]
else:
pass
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)
info = MVO.portfolio_info(np.array([1]), rets['Portfolio'].to_numpy().reshape(-1, 1), np.zeros(len(ret)-lookback))
info['username'] = session.get('username').split('@')[0]
info['role'] = role_map[role]
info['id']='使用者自行上傳'
info['name']='使用者自行上傳'
info['date'] = '-'
info['alpha'] = '-'
info['beta'] = '-'
info['r2'] = '-'
info['assets'] = list(port.columns)
# Plotting weight
fig = px.bar(weight)
fig['layout'] = {}
info['weight'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
# Plotting weight
fig = (rets+1).cumprod().iloc[::5, :].plot()
fig['layout'] = {}
info['ret'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
# Plotting ret bars
rets.index.name = 'date'
rets.index = pd.to_datetime(rets.index)
ret_hist = rets.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'] = {}
info['bar'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
return render_template('result_view.html', data=info)
@app.route('/result', methods=['GET', 'POST'])

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

@ -92,8 +92,6 @@ $sendPort.click(function(event) {
if (stockList.length > 1){
// $('#confirmMes').replaceWith("<span>投資組合已開始建立,請等待完成訊息,或1分鐘後至分析結果區查看!</span>")
// $('#confirmModal').modal('show');
$submitPort.prop('disabled', true);
$.ajax({
url: '/postPort', //todo create_strategy

@ -4,7 +4,91 @@
{% block title %}Strategy Page{% endblock%}
{% block content %}
<div class="comtainer-fluid" style="min-height:92%;position:relative;">
<h1>Still in progressing...</h1>
<div class="container-fluid" style="min-height:92%;position:relative;">
<!-- Button trigger modal -->
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
上傳檔案
</button>
<!-- Modal -->
<div class="modal fade" id="uploadModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title font-bold" style="color: #000055;">格式規範與上傳檔案</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<ul class="fa-ul">
<li><span class="fa-li"><i class="fa-solid fa-flag"></i></span>上傳之csv檔需包含header,且第一行為時間資訊。</li>
<li><span class="fa-li"><i class="fa-solid fa-flag"></i></span>價格資訊需長度相同,且資產數量大於1檔才會進行回測。</li>
<li><span class="fa-li"><i class="fa-solid fa-flag"></i></span>範例如下圖所示。</li>
</ul>
<img src="{{ url_for('static', filename='img/file.jpg') }}" class="img-fluid mb-3" alt="SINGUP IMAGE">
<form method="POST" enctype="multipart/form-data">
<div class="card my-3">
<div class="p-2 font-bold text-lg">
投組最佳化配置
</div>
<div class="input-group">
<span class="input-group-text bg-info">輸入數據時長</span>
<select name="lookback" class="form-select">
<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">
<span class="input-group-text bg-info">再平衡頻率</span>
<select name="frequency" class="form-select">
<option value="21">每月</option>
<option value="63">每季</option>
<option selected value="126">每半年</option>
<option value="252">每年</option>
</select>
</div>
<div class="input-group">
<span class="input-group-text bg-info">最佳化目標函數</span>
<select name="role" class="form-select" onchange="changeFunc(value);">
<option selected value="max_sharpe">最大化夏普比率</option>
<option value="max_sortino">最大化索提諾比率</option>
<option value="min_volatility">最小化波動率</option>
<option value="quadratic_utility">最大化效用函數</option>
</select>
</div>
<div class="input-group" style="display: none;" id="gamma">
<span class="input-group-text bg-info">風險厭惡係數</span>
<input type="number" id="gamma" name="gamma" name="targetAnnualVolatility" class="form-control fmt-pct" value="30" autocomplete="off">
<span class="input-group-text">%</span>
</div>
</div>
<div class="form-group d-flex">
<input type="file" class="form-control-file" id="csv_file" name="csv_file" accept=".csv" max-file="3" required>
<button id="uploadCheck" type="submit" class="btn btn-outline-primary ms-auto">確認上傳</button>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">關閉</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(document).ready(function(){
$("#uploadModal").modal('show');
});
function changeFunc(value) {
console.log(value);
if (value === 'quadratic_utility') {
$('#gamma').css("display", "flex");
} else {
$('#gamma').css("display", "none");
}
}
</script>
{% endblock script %}

@ -93,6 +93,9 @@
<th scope="col">Beta</th>
<th scope="col">VaR10</th>
<th scope="col">R2</th>
{% if data.role == '最大化效用函數' %}
<th scope="col">Gamma</th>
{% endif %}
</tr>
</thead>
<thead style="font-size: 1vmin'">
@ -105,6 +108,9 @@
<td>{{ data.beta }}</td>
<td>{{ data.var10 }}</td>
<td>{{ data.r2 }}</td>
{% if data.role == '最大化效用函數' %}
<td>{{ data.gamma }}</td>
{% endif %}
</tr>
</thead>
</table>

Loading…
Cancel
Save