投資組合大擂台 Ver. 2
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

622 lines
21 KiB

{% extends 'base.html' %}
{% block title %}Result View{% endblock %}
{% block style %}
<style>
.llm-advice-content {
font-size: 1rem;
color: #2d3748;
line-height: 1.7;
font-family: 'Segoe UI', 'Microsoft JhengHei', 'PingFang TC', sans-serif;
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
}
.llm-advice-content h5, .llm-advice-content h6 {
color: #1a365d;
background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
color: white;
padding: 12px 16px;
margin: 20px 0 16px 0;
border-radius: 8px;
font-weight: 600;
font-size: 1.1rem;
box-shadow: 0 2px 4px rgba(49, 130, 206, 0.2);
position: relative;
}
.llm-advice-content h5::before, .llm-advice-content h6::before {
content: "📊";
margin-right: 8px;
font-size: 1.2rem;
}
.llm-advice-content p {
margin-bottom: 16px;
text-align: left;
color: #4a5568;
font-weight: 400;
}
.llm-advice-content ul {
margin: 16px 0;
padding-left: 0;
}
.llm-advice-content li {
margin-bottom: 8px;
padding: 8px 12px;
background: #f7fafc;
border-left: 3px solid #3182ce;
border-radius: 4px;
list-style: none;
color: #2d3748;
transition: all 0.2s ease;
}
.llm-advice-content li:hover {
background: #edf2f7;
transform: translateX(2px);
}
.llm-advice-content strong {
color: #e53e3e;
font-weight: 600;
background: linear-gradient(135deg, #fed7d7 0%, #feb2b2 100%);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.95em;
}
.llm-advice-content em {
color: #805ad5;
font-style: italic;
font-weight: 500;
}
#llm-advice-container .alert {
border-radius: 12px;
margin-bottom: 0;
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 響應式設計 */
@media (max-width: 768px) {
.llm-advice-content {
padding: 16px;
font-size: 0.95rem;
}
.llm-advice-content h5, .llm-advice-content h6 {
font-size: 1rem;
padding: 10px 12px;
}
}
/* 載入動畫 */
.llm-advice-content.loading {
opacity: 0.7;
transition: opacity 0.3s ease;
}
/* Popover 樣式 */
.popover {
max-width: 300px;
}
.popover-body {
padding: 12px;
}
/* Markdown 代碼塊樣式 */
.llm-advice-content pre {
background: #f6f8fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 16px;
overflow-x: auto;
margin: 16px 0;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9em;
line-height: 1.45;
}
.llm-advice-content code {
background: #f6f8fa;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9em;
color: #24292e;
}
.llm-advice-content pre code {
background: transparent;
padding: 0;
border-radius: 0;
}
/* 表格樣式 */
.llm-advice-content table {
border-collapse: collapse;
width: 100%;
margin: 16px 0;
}
.llm-advice-content table th,
.llm-advice-content table td {
border: 1px solid #dfe2e5;
padding: 8px 12px;
}
.llm-advice-content table th {
background: #f6f8fa;
font-weight: 600;
}
/* 分隔線 */
.llm-advice-content hr {
border: none;
border-top: 2px solid #eaecef;
margin: 24px 0;
}
/* 引用塊 */
.llm-advice-content blockquote {
border-left: 4px solid #dfe2e5;
padding-left: 16px;
margin: 16px 0;
color: #6a737d;
}
</style>
{% endblock %}
{% block content %}
<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="alert alert-light text-dark" role="alert">
<div class="container">
<div class="row">
<div class="col-4">
<strong>策略編號</strong>
</div>
<div class="col-6">
{{ data.id|safe }}
</div>
</div>
<hr>
<div class="row">
<div class="col-4">
<strong>策略名稱</strong>
</div>
<div class="col-6">
{{ data.name|safe }}
</div>
</div>
<hr>
<div class="row">
<div class="col-4">
<strong>建立者</strong>
</div>
<div class="col-6">
{{ data.username|safe }}
</div>
</div>
<hr>
<div class="row">
<div class="col-4">
<strong>建立時間</strong>
</div>
<div class="col-6">
{{ data.date|safe }}
</div>
</div>
<hr>
<div class="row">
<div class="col-4">
<strong>策略目標</strong>
</div>
<div class="col-6">
{{ data.role|safe }}
</div>
</div>
<hr>
<div class="row">
<div class="col-4">
<strong>資產</strong>
</div>
<div class="col-6">
{% for a in data.assets %}
<span class="badge text-bg-warning asset" name="stock" value="{{ a|safe }}">{{ a|safe }}</span>
{% endfor %}
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('copy_portfolio') }}">
複製投資組合
</a>
</div>
</div>
<div class="row">
<button class="btn btn-secondary 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 m-3">
<div class="p-1 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">R2</th>
{% if data.role == '最大化效<EFBFBD><EFBFBD><EFBFBD>函數' %}
<th scope="col">Gamma</th>
{% endif %}
</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>
{% if data.role == '最大化效用函數' %}
<td>{{ data.gamma }}</td>
{% endif %}
</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="mb-4" id="weight" style="max-height:50vh"></div>
<div class="row justify-content-center font-bold text-xl">
投組價值走勢
</div>
<div class="mb-4" id="price" style="max-height:60vh"></div>
<div class="row justify-content-center font-bold text-xl">
投組季報酬率
</div>
<div class="mb-4" id="bar" style="max-height:60vh"></div>
<div class="row justify-content-center font-bold text-xl">
🤖 LLM 投組分析
</div>
<div class="card mb-4" style="border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
<div class="card-body">
<div id="llm-advice-container">
<div class="text-center text-muted">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">正在生成 LLM 投組分析...</p>
</div>
</div>
<!-- 重新生成按鈕組 -->
<div class="d-flex gap-2 mt-3 align-items-center flex-wrap">
<button id="refresh-advice" class="btn btn-outline-primary btn-sm" onclick="refreshLLMAdvice()">
🔄 重新生成分析
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{# <div class="container mt-4">
<form id="comment-form">
<div class="form-group">
<label for="username">輸入名稱:</label>
<input type="text" class="form-control" id="username" placeholder="Anonymous" value="Anonymous">
</div>
<div class="form-group">
<label for="comment">評論</label>
<textarea class="form-control" id="comment" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">Add Comment</button>
</form>
<div id="comments" class="mt-4"></div>
</div> #}
</div>
</div>
{% endblock %}
{% block script %}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<!-- Markdown 解析庫 -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- 代碼高亮庫 (可選) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<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': 10, 'b': 50},
'legend': {'yanchor': 'bottom', 'y': 1.3, 'xanchor': 'left', 'x': 0, 'orientation':'h',
'font': {'size':8}
}
};
const rlayout = {
'autosize': true,
'title': {'text': ''},
'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0], 'title':'', 'rangeslider': {'visible': true}
},
'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0], 'title':''},
'margin': {'l': 40, 'r': 20, 't': 30, 'b': 50},
'legend': {'yanchor': 'top', 'y': 1.3, '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': 40, 'r': 20, 't': 50, 'b': 70},
'legend': {'yanchor': 'top', 'y': 1.3, '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});
// LLM投資建議功能
const strategyId = {{ data.id }};
// 頁面載入時自動生成投資建議
document.addEventListener('DOMContentLoaded', function() {
refreshLLMAdvice();
// 初始化 popover
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
});
async function refreshLLMAdvice() {
const container = document.getElementById('llm-advice-container');
const button = document.getElementById('refresh-advice');
try {
// 禁用按鈕
button.disabled = true;
button.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 正在生成...';
// 顯示loading狀態
container.innerHTML = `
<div class="text-center text-muted">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">正在生成 LLM 投組分析...</p>
</div>
`;
// 調用API,使用 CoT(更詳細的思考過程)
const url = `/api/llm_advice/${strategyId}`;
const response = await fetch(url);
const result = await response.json();
if (result.success) {
// 格式化並顯示建議
const formattedAdvice = formatLLMAdvice(result.advice);
container.innerHTML = formattedAdvice;
} else {
container.innerHTML = `
<div class="alert alert-warning">
<h6>⚠ 無法生成 LLM 投組分析</h6>
<p>${result.error}</p>
${result.details ? `<small class="text-muted">${result.details}</small>` : ''}
</div>
`;
}
} catch (error) {
container.innerHTML = `
<div class="alert alert-danger">
<h6>❌ 發生錯誤</h6>
<p>無法連接到 LLM 投組分析服務,請稍後再試。</p>
<small class="text-muted">錯誤詳情:${error.message}</small>
</div>
`;
} finally {
// 恢復按鈕
button.disabled = false;
button.innerHTML = '🔄 重新生成分析';
}
}
function formatLLMAdvice(advice) {
// 使用 marked.js 解析 Markdown
if (typeof marked !== 'undefined') {
// 配置 marked
marked.setOptions({
breaks: true, // 支援換行
gfm: true, // GitHub Flavored Markdown
headerIds: false, // 不生成 header ID
mangle: false // 不編碼 email
});
try {
const html = marked.parse(advice);
// 包裝在容器中
return `
<div class="llm-advice-content markdown-body">
${html}
</div>
`;
} catch (e) {
console.error('Markdown parsing error:', e);
// Fallback to simple formatting
return `
<div class="llm-advice-content">
<pre style="white-space: pre-wrap; font-family: inherit;">${advice}</pre>
</div>
`;
}
} else {
// Fallback: 簡單的 Markdown 解析 (如果 marked.js 沒載入)
const lines = advice.split('\n');
let html = '';
let inCodeBlock = false;
let codeContent = '';
for (let line of lines) {
// 代碼塊處理
if (line.trim().startsWith('```')) {
if (inCodeBlock) {
// 結束代碼塊
html += `<pre class="code-block"><code>${escapeHtml(codeContent)}</code></pre>`;
codeContent = '';
inCodeBlock = false;
} else {
// 開始代碼塊
inCodeBlock = true;
}
continue;
}
if (inCodeBlock) {
codeContent += line + '\n';
continue;
}
line = line.trim();
if (!line) {
html += '<br>';
continue;
}
// Markdown 處理
line = line.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); // 粗體
line = line.replace(/\*([^*]+)\*/g, '<em>$1</em>'); // 斜體
line = line.replace(/`([^`]+)`/g, '<code>$1</code>'); // 行內代碼
// 標題
if (line.match(/^#{1,3}\s+/)) {
const level = line.match(/^#+/)[0].length;
const text = line.replace(/^#+\s+/, '');
html += `<h${level + 4} class="mt-3 mb-2">${text}</h${level + 4}>`;
}
// 列表
else if (line.startsWith('-') || line.startsWith('•') || line.startsWith('*')) {
html += `<li>${line.substring(1).trim()}</li>`;
}
// 數字列表
else if (line.match(/^\d+\./)) {
html += `<li>${line}</li>`;
}
// 普通段落
else {
html += `<p>${line}</p>`;
}
}
return `
<div class="llm-advice-content">
${html}
</div>
`;
}
}
// HTML 轉義函數
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 生成指定模式的建議
async function generateAdviceWithMode(mode) {
const container = document.getElementById('llm-advice-container');
const button = document.getElementById('refresh-advice');
try {
// 禁用按鈕
button.disabled = true;
button.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 正在生成...';
// 顯示loading狀態
container.innerHTML = `
<div class="text-center text-muted">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">正在生成${getModeDescription(mode)}...</p>
</div>
`;
// 調用API
const response = await fetch(`/api/llm_advice/${strategyId}?mode=${mode}`);
const result = await response.json();
if (result.success) {
// 格式化並顯示建議
const formattedAdvice = formatLLMAdvice(result.advice);
container.innerHTML = formattedAdvice;
} else {
container.innerHTML = `
<div class="alert alert-warning">
<h6>⚠ 無法生成 LLM 投組分析</h6>
<p>${result.error}</p>
${result.details ? `<small class="text-muted">${result.details}</small>` : ''}
</div>
`;
}
} catch (error) {
container.innerHTML = `
<div class="alert alert-danger">
<h6>❌ 發生錯誤</h6>
<p>無法連接到 LLM 投組分析服務,請稍後再試。</p>
<small class="text-muted">錯誤詳情:${error.message}</small>
</div>
`;
} finally {
// 恢復按鈕
button.disabled = false;
button.innerHTML = '🔄 重新生成建議';
}
}
// 獲取模<EFBFBD><EFBFBD>描述
function getModeDescription(mode) {
const descriptions = {
'simple': '快速分析',
'comprehensive': '深度分析',
'risk': '風險分析'
};
return descriptions[mode] || '投資建議';
}
</script>
{% endblock script %}