[UX] Remove accordion, restore detailed prompt, keep popover options

- Removed accordion functionality as content became too sparse
- Restored detailed prompt structure from llm_service.py
- Enhanced fallback analysis with comprehensive metrics (VaR, R2, etc)
- Kept popover options for different analysis modes
- Updated max_tokens to 3000 for more detailed responses
data-init-fixes
Eric0801 1 month ago
parent 14407e04d2
commit 1c16cb4321
  1. 14
      docker-compose.yml
  2. 108
      llm_service.py
  3. 99
      templates/result_view.html

@ -12,6 +12,8 @@ services:
- db_data_new:/var/lib/postgresql/data
networks:
- common_network
ports:
- 5432:5432
redis:
image: redis:7.0.11-alpine
@ -34,6 +36,18 @@ services:
container_name: flask
command: bash -c "cd ../flask_run ; python main.py runserver 0.0.0.0:8000"
image: tpm-flask
environment:
- LLM_PROVIDER=openrouter
- OPENROUTER_API_KEY=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
- OPENROUTER_MODEL=google/gemini-2.0-flash-exp:free
- OPENROUTER_REFERER=http://localhost:8007
- OPENROUTER_TITLE=TPM
- LLM_TIMEOUT=60
- LLM_MAX_TOKENS=1500
- LLM_TEMPERATURE=0.6
- LLM_MAX_RETRIES=3
- LLM_RETRY_DELAY=2
- MOCK_LLM=true
volumes:
- flask-data:/flask_run
depends_on:

@ -23,7 +23,7 @@ except Exception:
'api_key': os.environ.get('OPENAI_API_KEY') or os.environ.get('OPENROUTER_API_KEY', ''),
'model': os.environ.get('OPENAI_MODEL', os.environ.get('OPENROUTER_MODEL', 'gpt-4')),
'timeout': int(os.environ.get('LLM_TIMEOUT', '60')),
'max_tokens': int(os.environ.get('LLM_MAX_TOKENS', '2000')),
'max_tokens': int(os.environ.get('LLM_MAX_TOKENS', '3000')),
'temperature': float(os.environ.get('LLM_TEMPERATURE', '0.7'))
}
RATE_LIMITS = {
@ -258,25 +258,107 @@ class LLMInvestmentAdvisor:
raise e
def _get_fallback_advice(self, strategy_data: Dict[str, Any]) -> str:
"""獲取fallback投資建議"""
"""獲取fallback投資建議 - 使用原本的詳細 prompt 結構"""
try:
# 嘗試使用 prompts/investment_advice.py 的模板
from prompts.investment_advice import get_comprehensive_analysis_prompt
# 直接返回 prompt 作為 fallback(模擬 LLM 回覆的結構)
prompt = get_comprehensive_analysis_prompt(strategy_data)
# 提取 prompt 中的策略數據部分,生成基本回覆
return self._generate_basic_analysis(strategy_data)
except ImportError:
return self._generate_basic_analysis(strategy_data)
def _generate_basic_analysis(self, strategy_data: Dict[str, Any]) -> str:
"""生成基本分析(當無法調用 LLM 時)"""
annual_ret = strategy_data.get('annual_ret', 0)
vol = strategy_data.get('vol', 0)
sharpe = strategy_data.get('annual_sr', 0)
mdd = strategy_data.get('mdd', 0)
alpha = strategy_data.get('alpha', 0)
beta = strategy_data.get('beta', 0)
var = strategy_data.get('var10', 0)
r2 = strategy_data.get('r2', 0)
assets = strategy_data.get('assets', [])
# 評估表現等級
ret_level = "優秀" if annual_ret > 0.15 else "良好" if annual_ret > 0.08 else "一般" if annual_ret > 0 else "需改善"
risk_level = "低風險" if vol < 0.15 else "中風險" if vol < 0.25 else "高風險"
sharpe_level = "優良" if sharpe > 1.5 else "良好" if sharpe > 1.0 else "一般" if sharpe > 0.5 else "需改善"
return f"""
基於您的投資策略數據我提供以下初步分析
## 📊 策略總評
這是一個{ret_level}的投資策略年化報酬率達到{annual_ret:.1%}屬於{risk_level}等級策略的風險調整後報酬表現{sharpe_level}夏普比率為{sharpe:.2f}顯示出{"良好" if sharpe > 1.0 else "一般"}的風險管理能力
## 📈 關鍵指標深度解析
### 💰 報酬表現
- **年化報酬率**{annual_ret:.2%} - 這個報酬率在當前市場環境下表現{'優異' if annual_ret > 0.12 else '良好' if annual_ret > 0.08 else '一般'}{'超越' if annual_ret > 0.1 else '接近' if annual_ret > 0.05 else '低於'}市場平均水準
- **風險調整報酬**{sharpe:.2f} - 夏普比率{'超過1.0' if sharpe > 1.0 else '接近1.0' if sharpe > 0.8 else '低於1.0'}表示每承擔一單位風險能獲得{'良好' if sharpe > 1.0 else '一般'}的超額報酬
### ⚠ 風險評估
- **波動率**{vol:.2%} - 年化波動率{'較低' if vol < 0.15 else '適中' if vol < 0.25 else '較高'}顯示策略的穩定性{'良好' if vol < 0.2 else '一般'}
- **最大回落**{mdd:.2%} - 最大虧損幅度{'控制在合理範圍' if abs(mdd) < 0.1 else '需要關注'}風險控制{'得當' if abs(mdd) < 0.15 else '有待改善'}
- **VaR (95%)**{var:.2%} - 在95%的信心水準下單日最大可能虧損為{abs(var):.2%}
### 📊 市場關聯性
- **Alpha值**{alpha:.4f} - {'正向' if alpha > 0 else '負向'}超額收益顯示策略{'優於' if alpha > 0 else '弱於'}市場基準表現
- **Beta值**{beta:.2f} - 策略與市場的關聯性{'較高' if beta > 1.1 else '適中' if beta > 0.9 else '較低'}市場波動1%策略預期波動{beta:.2f}%
- **R-squared**{r2:.2%} - 策略表現有{r2:.1%}可由市場變動解釋
## 🎯 策略優劣分析
### 👍 亮點優勢
- **風險調整報酬{'優異' if sharpe > 1.5 else '良好' if sharpe > 1.0 else '一般'}**夏普比率{sharpe:.2f}顯示策略在控制風險的同時能創造{'優質' if sharpe > 1.5 else '良好' if sharpe > 1.0 else '基本'}的報酬
- **資產配置{'合理' if len(assets) > 3 else '需強化'}**包含{len(assets)}個標的{'有效' if len(assets) > 5 else '適度' if len(assets) > 3 else '需增加'}分散投資風險
- **市場適應性**Alpha值{alpha:.4f}顯示策略{'具有' if alpha > 0 else '缺乏'}超額收益能力
### 🤔 潛在風險
- **市場敏感度**Beta值{beta:.2f}表示策略{'高度' if beta > 1.2 else '適度' if beta > 0.8 else '低度'}受市場波動影響
- **回檔風險**最大回落{mdd:.2%}顯示在極端市場情況下可能面臨{'較大' if abs(mdd) > 0.2 else '中等' if abs(mdd) > 0.1 else '有限'}的虧損
- **集中度風險**需要{'特別' if len(assets) < 5 else '持續'}關注資產配置是否過於集中在特定行業或地區
## 💡 具體投資建議
### 1. 核心觀點
**建議**{'✅ 繼續持有並定期調整' if annual_ret > 0.1 and sharpe > 1.0 else ' 考慮優化配置' if annual_ret > 0.05 else '❌ 需要重新評估策略'}
**理由**
- 報酬表現{'符合' if annual_ret > 0.08 else '未達'}預期目標
- 風險控制{'良好' if sharpe > 1.0 else '需要改善'}
- 市場適應性{'優異' if alpha > 0.02 else '適中' if alpha > 0 else '不佳'}
### 2. 優化方向
- **資產配置調整**可考慮納入更多{'防禦性資產(如公債、公用事業)' if vol > 0.2 else '成長性資產(如科技股、新興市場)'}{'降低整體風險' if vol > 0.2 else '提升報酬潛力'}
- **再平衡頻率**建議將再平衡頻率調整為{'每月' if vol > 0.25 else '每季度' if vol > 0.15 else '每半年'}一次以適應市場變化
- **風險暴露管理**{'考慮降低' if beta > 1.2 else '可適度提高' if beta < 0.8 else '維持當前'}市場Beta暴露
### 3. 風險管理
- **止損設定**建議設定止損點在{abs(mdd)*1.3:.1%}當前最大回落的1.3避免過大虧損
- **倉位控制**在市場波動加劇時{'建議降低倉位至70-80%' if vol > 0.25 else '可維持滿倉' if vol < 0.15 else '維持80-90%倉位'}
- **監控指標**
- 持續關注VaR指標當VaR超過{abs(var)*1.5:.2%}時應考慮降低風險
- 密切追蹤市場波動性變化VIX指數等
- 定期檢視Alpha表現確保策略持續創造超額收益
## 🔮 未來展望
基於當前市場環境和策略表現預期未來3-6個月內
- **報酬預期**策略將維持{'穩定增長' if sharpe > 1.2 else '波動' if sharpe > 0.8 else '震盪'}表現年化報酬率預計在{annual_ret*0.8:.1%}{annual_ret*1.2:.1%}之間
- **風險展望**市場波動性{'可能上升' if vol > 0.2 else '預期維持穩定'}建議投資者{'提高警覺' if vol > 0.2 else '保持耐心'}
- **調整建議**定期檢視建議每{'' if vol > 0.25 else '季度'}一次並適時調整以應對市場變化
📊 **表現評估**
- 年化報酬率{annual_ret:.2%} - {'表現良好' if annual_ret > 0.1 else '有改進空間'}
- 年化波動率{vol:.2%} - {'風險適中' if vol < 0.2 else '風險較高'}
- 夏普比率{sharpe:.2f} - {'風險調整後報酬優良' if sharpe > 1 else '有改進空間'}
### 關鍵提醒
**免責聲明**此為基於歷史數據的模擬分析結果過去績效不代表未來表現實際投資請謹慎評估個人風險承受能力並建議諮詢專業投資顧問
💡 **初步建議**
1. 持續監控市場變化
2. 定期檢視投資組合配置
3. 考慮分散投資降低風險
📝 **行動建議**
1. 定期監控策略表現建議每週檢視一次
2. 設定明確的投資目標和停損點
3. 保持投資組合的靈活性適時調整
4. 持續學習市場動態提升投資判斷力
*注意此為預設建議如需詳細分析請稍後再試*
*本分析報告生成於{strategy_data.get('name', 'N/A')}策略僅供參考*
"""
# Duplicate clear_cache removed

@ -103,6 +103,15 @@
opacity: 0.7;
transition: opacity 0.3s ease;
}
/* Popover 樣式 */
.popover {
max-width: 300px;
}
.popover-body {
padding: 12px;
}
</style>
{% endblock %}
@ -244,9 +253,25 @@
<p class="mt-2">正在生成AI投資建議...</p>
</div>
</div>
<button id="refresh-advice" class="btn btn-outline-primary btn-sm mt-2" onclick="refreshLLMAdvice()">
🔄 重新生成建議
</button>
<!-- 重新生成按鈕組 -->
<div class="d-flex gap-2 mt-3">
<button id="refresh-advice" class="btn btn-outline-primary btn-sm" onclick="refreshLLMAdvice()">
🔄 重新生成建議
</button>
<!-- Popover 選項按鈕 -->
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="popover"
data-bs-placement="top" data-bs-html="true"
data-bs-content="<div class='mb-2'><strong>選擇分析深度:</strong></div>
<div class='d-grid gap-1'>
<button class='btn btn-sm btn-outline-info' onclick='generateAdviceWithMode(\"simple\")'>📊 快速分析</button>
<button class='btn btn-sm btn-outline-warning' onclick='generateAdviceWithMode(\"comprehensive\")'>🔍 深度分析</button>
<button class='btn btn-sm btn-outline-danger' onclick='generateAdviceWithMode(\"risk\")'> 風險分析</button>
</div>">
分析選項
</button>
</div>
</div>
</div>
</div>
@ -316,6 +341,12 @@
// 頁面載入時自動生成投資建議
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() {
@ -412,5 +443,67 @@
</div>
`;
}
// 生成指定模式的建議
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> 無法生成投資建議</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>無法連接到投資建議服務,請稍後再試。</p>
<small class="text-muted">錯誤詳情:${error.message}</small>
</div>
`;
} finally {
// 恢復按鈕
button.disabled = false;
button.innerHTML = '🔄 重新生成建議';
}
}
// 獲取模式描述
function getModeDescription(mode) {
const descriptions = {
'simple': '快速分析',
'comprehensive': '深度分析',
'risk': '風險分析'
};
return descriptions[mode] || '投資建議';
}
</script>
{% endblock script %}
Loading…
Cancel
Save