Add LLM investment advice feature with OpenAI integration\n\n- Created llm_service.py with comprehensive LLM advisor\n- Added config_openai.py for API configuration\n- Created prompts/investment_advice.py with prompt templates\n- Updated requirements.txt with OpenAI dependency\n- Modified main.py with /api/llm_advice endpoint\n- Enhanced result_view.html with LLM advice section\n- Added CSS styling for better UI\n- Created LLM_SETUP.md setup guide\n- Added test_llm_service.py for testing\n- Implemented caching, retry logic, and error handling\n- Features: strategy analysis, risk assessment, market insights

data-init-fixes
Eric0801 2 months ago
parent fd39ea0287
commit 45e9ea7ec2
  1. 116
      LLM_SETUP.md
  2. 30
      config_openai.py
  3. 248
      llm_service.py
  4. 57
      main.py
  5. 159
      prompts/investment_advice.py
  6. 1
      requirements.txt
  7. 152
      templates/result_view.html
  8. 194
      test_llm_service.py

@ -0,0 +1,116 @@
# LLM 投資建議功能設置指南
## 功能介紹
LLM 投資建議功能使用 OpenAI GPT 模型為投資組合提供專業的投資分析和建議,包括:
- 策略績效評估
- 風險分析
- 市場適配性評估
- 具體的改進建議
## 設置步驟
### 1. 獲取 OpenAI API 金鑰
1. 前往 [OpenAI API](https://platform.openai.com/api-keys) 頁面
2. 點擊 "Create new secret key"
3. 複製生成的 API 金鑰
### 2. 配置 API 金鑰
您可以通過以下兩種方式之一設置 API 金鑰:
#### 方法一:環境變數(推薦)
```bash
export OPENAI_API_KEY="your-api-key-here"
```
#### 方法二:修改配置檔案
編輯 `config_openai.py` 檔案:
```python
OPENAI_CONFIG = {
'api_key': 'your-actual-api-key-here',
# ... 其他配置
}
```
### 3. 安裝依賴
```bash
pip install -r requirements.txt
```
### 4. 啟動服務
```bash
docker compose up -d flask
```
### 5. 測試功能
1. 訪問任意策略詳情頁面
2. 查看「🤖 LLM 投資建議」區塊
3. 系統會自動生成 AI 投資建議
## 配置選項
### OpenAI 模型選擇
`config_openai.py` 中可以調整:
```python
OPENAI_CONFIG = {
'model': 'gpt-4', # 選擇 gpt-3.5-turbo 可降低成本
'max_tokens': 2000, # 最大回應長度
'temperature': 0.7, # 創意程度 (0-1)
}
```
### 快取設定
```python
CACHE_CONFIG = {
'enabled': True, # 啟用快取
'ttl': 3600, # 快取時間(秒)
'max_size': 100, # 最大快取項目數
}
```
## 費用估算
- **GPT-4**: 約 $0.03/1K tokens
- **GPT-3.5-turbo**: 約 $0.002/1K tokens
每個投資建議約使用 1000-2000 tokens,建議選擇合適的模型以控制成本。
## 故障排除
### 常見問題
1. **API 金鑰錯誤**
- 檢查 API 金鑰是否正確設置
- 確認金鑰沒有額外的空格
2. **連接到服務失敗**
- 檢查 Flask 服務是否正常運行
- 確認防火牆設定允許內部通訊
3. **生成建議失敗**
- 查看後端日誌確認錯誤原因
- 檢查 OpenAI API 額度是否充足
### 檢查日誌
```bash
docker compose logs flask
```
## 進階配置
如需自訂 Prompt 模板,請編輯 `prompts/investment_advice.py` 檔案中的模板函數。
## 安全性注意事項
- 請勿將 API 金鑰提交到版本控制系統
- 考慮使用環境變數而非硬編碼
- 定期輪換 API 金鑰

@ -0,0 +1,30 @@
"""
OpenAI API 配置
請設置您的OpenAI API金鑰
"""
import os
# OpenAI API 配置
OPENAI_CONFIG = {
'api_key': os.getenv('OPENAI_API_KEY', 'your-api-key-here'),
'model': 'gpt-4', # 可選擇 gpt-3.5-turbo 以降低成本
'max_tokens': 2000,
'temperature': 0.7,
'timeout': 30, # 請求超時時間(秒)
}
# API 調用限制
RATE_LIMITS = {
'requests_per_minute': 60, # 每分鐘最大請求數
'max_retries': 3, # 最大重試次數
'retry_delay': 2, # 重試間隔(秒)
}
# 快取設定
CACHE_CONFIG = {
'enabled': True,
'ttl': 3600, # 快取時間(秒)
'max_size': 100, # 最大快取項目數
}

@ -0,0 +1,248 @@
"""
LLM Investment Advisor Service
提供投資策略的AI分析服務包含
- 投資建議生成
- 風險評估
- 市場洞察
- Prompt工程管理
"""
import os
import json
import time
import logging
from typing import Dict, Any, Optional, Tuple
from functools import lru_cache
import time
import openai
from openai import OpenAI
from config_openai import OPENAI_CONFIG, RATE_LIMITS
# 設定日誌
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class PromptManager:
"""管理不同的Prompt模板"""
def __init__(self):
self.system_prompt = self._get_system_prompt()
def _get_system_prompt(self) -> str:
"""系統提示詞"""
return """你是一位經驗豐富的專業投資顧問,擁有超過15年的投資經驗和深厚的金融知識。
請基於提供的投資組合數據提供專業客觀且實用的投資建議
請從以下幾個面向進行分析
1. 整體表現評估年化報酬風險指標
2. 風險收益特性分析夏普比率最大回落
3. 市場環境適配性評估
4. 具體的改進建議和操作建議
5. 風險管理和再平衡建議
請確保你的回答
- 專業且易懂避免過度技術術語
- 基於數據事實客觀分析
- 提供可操作的具體建議
- 考慮台灣市場的特殊性如果適用"""
def build_strategy_context(self, strategy_data: Dict[str, Any]) -> str:
"""將策略資料轉換為結構化context"""
return f"""
策略基本資訊
- 策略編號{strategy_data.get('id', 'N/A')}
- 策略名稱{strategy_data.get('name', 'N/A')}
- 投資目標{strategy_data.get('role', 'N/A')}
- 建立時間{strategy_data.get('date', 'N/A')}
- 建立者{strategy_data.get('username', 'N/A')}
績效指標
- 年化報酬率{strategy_data.get('annual_ret', 0):.2%}
- 年化波動率{strategy_data.get('vol', 0):.2%}
- 年化夏普比率{strategy_data.get('annual_sr', 0):.2f}
- 最大回落MDD{strategy_data.get('mdd', 0):.2%}
- Alpha值{strategy_data.get('alpha', 0):.4f}
- Beta值{strategy_data.get('beta', 0):.4f}
- VaR (10%){strategy_data.get('var10', 0):.2%}
- R-squared{strategy_data.get('r2', 0):.4f}
- Gamma值{strategy_data.get('gamma', 0):.4f}
投資組合配置
- 包含資產{', '.join(strategy_data.get('assets', []))}
- 市場類型{'台灣市場' if strategy_data.get('tw', True) else '美國市場'}
"""
def get_investment_advice_prompt(self, strategy_data: Dict[str, Any]) -> str:
"""生成投資建議的完整Prompt"""
context = self.build_strategy_context(strategy_data)
return f"""{self.system_prompt}
{context}
請提供詳細的投資建議分析包含
1. 整體表現評估該策略的強項和弱點
2. 風險評估當前風險水平的評價和建議
3. 市場適配性該策略在當前市場環境下的適配程度
4. 改進建議具體的可操作改進建議
5. 未來展望未來3-6個月的投資建議
請用繁體中文回答結構清晰建議具體可行"""
class LLMInvestmentAdvisor:
"""LLM投資顧問主類"""
def __init__(self, api_key: Optional[str] = None):
"""初始化LLM服務"""
self.api_key = api_key or OPENAI_CONFIG['api_key']
if not self.api_key or self.api_key == 'your-api-key-here':
raise ValueError("OpenAI API key is required. Please set OPENAI_API_KEY environment variable.")
self.client = OpenAI(
api_key=self.api_key,
timeout=OPENAI_CONFIG.get('timeout', 30)
)
self.prompt_manager = PromptManager()
self.model = OPENAI_CONFIG.get('model', 'gpt-4')
self.max_tokens = OPENAI_CONFIG.get('max_tokens', 2000)
self.temperature = OPENAI_CONFIG.get('temperature', 0.7)
# 快取設定
self.cache: Dict[str, Tuple[str, float]] = {}
self.cache_timeout = 3600 # 1小時快取
def _is_cache_valid(self, cache_time: float) -> bool:
"""檢查快取是否有效"""
return time.time() - cache_time < self.cache_timeout
@lru_cache(maxsize=100)
def generate_advice(self, strategy_id: str, strategy_data: Dict[str, Any]) -> str:
"""生成投資建議
Args:
strategy_id: 策略ID
strategy_data: 策略資料字典
Returns:
str: 投資建議文本
"""
cache_key = f"advice_{strategy_id}_{hash(str(strategy_data))}"
# 檢查快取
if cache_key in self.cache:
advice, cache_time = self.cache[cache_key]
if self._is_cache_valid(cache_time):
logger.info(f"Returning cached advice for strategy {strategy_id}")
return advice
try:
# 構建prompt
prompt = self.prompt_manager.get_investment_advice_prompt(strategy_data)
# 調用OpenAI API
response = self._call_openai_with_retry(prompt)
# 快取結果
self.cache[cache_key] = (response, time.time())
logger.info(f"Generated new advice for strategy {strategy_id}")
return response
except Exception as e:
logger.error(f"Error generating advice for strategy {strategy_id}: {str(e)}")
return self._get_fallback_advice(strategy_data)
def _call_openai_with_retry(self, prompt: str) -> str:
"""調用OpenAI API,包含重試機制
Args:
prompt: 完整的prompt
Returns:
str: API回應內容
"""
max_retries = RATE_LIMITS.get('max_retries', 3)
retry_delay = RATE_LIMITS.get('retry_delay', 2)
for attempt in range(max_retries):
try:
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "你是一位專業的投資顧問。"},
{"role": "user", "content": prompt}
],
max_tokens=self.max_tokens,
temperature=self.temperature,
top_p=0.9
)
return response.choices[0].message.content.strip()
except openai.RateLimitError as e:
if attempt < max_retries - 1:
# 指數退避:1s, 2s, 4s
wait_time = retry_delay ** attempt
logger.warning(f"Rate limit exceeded, retrying in {wait_time}s... (attempt {attempt + 1}/{max_retries})")
time.sleep(wait_time)
continue
else:
logger.error(f"Rate limit exceeded after {max_retries} attempts")
raise e
except openai.APIError as e:
if attempt < max_retries - 1:
wait_time = retry_delay ** attempt
logger.warning(f"API error, retrying in {wait_time}s... (attempt {attempt + 1}/{max_retries}): {str(e)}")
time.sleep(wait_time)
continue
else:
logger.error(f"API error after {max_retries} attempts: {str(e)}")
raise e
except Exception as e:
logger.error(f"Unexpected error calling OpenAI: {str(e)}")
raise e
def _get_fallback_advice(self, strategy_data: Dict[str, Any]) -> str:
"""獲取fallback投資建議"""
annual_ret = strategy_data.get('annual_ret', 0)
vol = strategy_data.get('vol', 0)
sharpe = strategy_data.get('annual_sr', 0)
return f"""
基於您的投資策略數據我提供以下初步分析
📊 **表現評估**
- 年化報酬率{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. 考慮分散投資降低風險
*注意此為預設建議如需詳細分析請稍後再試*
"""
def clear_cache(self):
"""清除快取"""
self.cache.clear()
self.generate_advice.cache_clear()
logger.info("LLM advice cache cleared")
# 創建全域實例
llm_advisor = None
def get_llm_advisor() -> LLMInvestmentAdvisor:
"""獲取LLM顧問實例"""
global llm_advisor
if llm_advisor is None:
llm_advisor = LLMInvestmentAdvisor()
return llm_advisor

@ -15,6 +15,7 @@ import random
import string
import logging
import numpy as np
logger = logging.getLogger(__name__)
import pandas as pd
import psycopg2
import psycopg2.extras
@ -22,6 +23,7 @@ import plotly
import plotly.express as px
from portfolio_builder import MVO
from config import *
from llm_service import get_llm_advisor
pd.options.plotting.backend = "plotly"
@ -524,6 +526,61 @@ def result_view():
data['bar'] = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
session['currStockList'] = data['assets']
return render_template('result_view.html', data=data)
@app.route('/api/llm_advice/<int:strategy_id>')
def llm_advice(strategy_id):
"""LLM投資建議API
接收策略ID返回AI生成的投資建議
"""
try:
# 驗證登入
if not login_required():
return jsonify({
'success': False,
'error': '請先登入'
}), 401
# 查詢策略資料
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, (strategy_id,))
strategy_data = curs.fetchone()
conn.close()
if not strategy_data:
return jsonify({
'success': False,
'error': '策略不存在'
}), 404
# 轉換為字典並處理角色映射
strategy_dict = dict(strategy_data)
strategy_dict['role'] = role_map.get(strategy_dict['role'], strategy_dict['role'])
# 獲取LLM顧問實例
llm_advisor = get_llm_advisor()
# 生成投資建議
advice = llm_advisor.generate_advice(str(strategy_id), strategy_dict)
return jsonify({
'success': True,
'advice': advice,
'strategy_id': strategy_id
})
except Exception as e:
logger.error(f"Error generating LLM advice for strategy {strategy_id}: {str(e)}")
return jsonify({
'success': False,
'error': '生成建議時發生錯誤',
'details': str(e)
}), 500
@app.route('/copy_portfolio')
def copy_portfolio():
if login_required():

@ -0,0 +1,159 @@
"""
投資建議相關的Prompt模板
包含
- 基本投資建議模板
- 進階分析模板
- 不同市場環境的模板
"""
from typing import Dict, Any
def get_basic_investment_prompt(strategy_data: Dict[str, Any]) -> str:
"""基本投資建議Prompt"""
return f"""
請基於以下投資策略數據提供專業的投資建議
策略概況
- 年化報酬率{strategy_data.get('annual_ret', 0):.2%}
- 年化波動率{strategy_data.get('vol', 0):.2%}
- 夏普比率{strategy_data.get('annual_sr', 0):.2f}
- 最大回落{strategy_data.get('mdd', 0):.2%}
- 投資目標{strategy_data.get('role', 'N/A')}
投資組合包含{', '.join(strategy_data.get('assets', []))}
請提供
1. 整體表現評估
2. 風險收益分析
3. 改進建議
"""
def get_comprehensive_analysis_prompt(strategy_data: Dict[str, Any]) -> str:
"""全面分析Prompt"""
return f"""
作為專業投資顧問請對以下投資策略進行全面分析並提供建議
策略基本資訊
- 策略名稱{strategy_data.get('name', 'N/A')}
- 投資目標{strategy_data.get('role', 'N/A')}
- 市場類型{'台灣市場' if strategy_data.get('tw', True) else '美國市場'}
關鍵績效指標
- 年化報酬率{strategy_data.get('annual_ret', 0):.2%}
- 年化波動率{strategy_data.get('vol', 0):.2%}
- 年化夏普比率{strategy_data.get('annual_sr', 0):.2f}
- 最大回落MDD{strategy_data.get('mdd', 0):.2%}
- Alpha值{strategy_data.get('alpha', 0):.4f}
- Beta值{strategy_data.get('beta', 0):.4f}
- VaR (95%){strategy_data.get('var10', 0):.2%}
- R-squared{strategy_data.get('r2', 0):.4f}
投資組合配置
持有資產{', '.join(strategy_data.get('assets', []))}
權重配置{strategy_data.get('weight', {{}}).get('columns', [])}
分析要求
請從以下面向提供詳細分析和建議
1. **績效評估**
- 與市場基準比較台灣加權指數或S&P 500
- 歷史表現趨勢分析
- 相對表現評價
2. **風險分析**
- 整體風險水平評估
- 主要風險來源識別
- 風險調整後報酬分析
3. **市場適配性**
- 當前市場環境適配程度
- 經濟週期位置評估
- 市場波動性影響分析
4. **投資建議**
- 資產配置優化建議
- 風險管理改進方案
- 定期調整和再平衡建議
5. **未來展望**
- 未來3-6個月投資展望
- 潛在風險預警
- 操作建議和時機點
請用專業客觀且易懂的語言回答使用繁體中文
"""
def get_risk_focused_prompt(strategy_data: Dict[str, Any]) -> str:
"""風險導向分析Prompt"""
return f"""
請重點分析以下投資策略的風險特性並提供風險管理建議
風險指標
- 年化波動率{strategy_data.get('vol', 0):.2%}
- 最大回落{strategy_data.get('mdd', 0):.2%}
- Beta值{strategy_data.get('beta', 0):.4f}
- VaR (10%){strategy_data.get('var10', 0):.2%}
- 夏普比率{strategy_data.get('annual_sr', 0):.2f}
風險分析要求
1. 評估當前風險水平//
2. 識別主要風險來源
3. 分析風險與報酬的配比
4. 提供風險降低策略
5. 建議風險監控指標
請提供具體可操作的風險管理建議
"""
def get_market_context_prompt(strategy_data: Dict[str, Any], market_condition: str = "normal") -> str:
"""市場環境特定Prompt"""
market_contexts = {
"bull": "目前市場處於牛市環境",
"bear": "目前市場處於熊市環境",
"volatile": "目前市場波動劇烈",
"normal": "目前市場環境正常"
}
context = market_contexts.get(market_condition, market_contexts["normal"])
return f"""
{context}請針對以下投資策略提供相應的投資建議
策略資訊
- 年化報酬率{strategy_data.get('annual_ret', 0):.2%}
- 年化波動率{strategy_data.get('vol', 0):.2%}
- 投資目標{strategy_data.get('role', 'N/A')}
市場適配建議
根據當前市場環境請分析
1. 該策略在當前環境下的適配程度
2. 可能的調整建議
3. 風險控制措施
4. 時機選擇建議
請提供針對當前市場環境的具體操作建議
"""
def build_custom_prompt(strategy_data: Dict[str, Any], analysis_type: str, **kwargs) -> str:
"""自訂Prompt生成器
Args:
strategy_data: 策略資料
analysis_type: 分析類型 ('basic', 'comprehensive', 'risk', 'market')
**kwargs: 額外參數
"""
templates = {
'basic': get_basic_investment_prompt,
'comprehensive': get_comprehensive_analysis_prompt,
'risk': get_risk_focused_prompt,
'market': lambda data: get_market_context_prompt(data, kwargs.get('condition', 'normal'))
}
template_func = templates.get(analysis_type, get_basic_investment_prompt)
return template_func(strategy_data)

@ -18,3 +18,4 @@ tqdm==4.62.3
werkzeug==2.2.2
schedule==1.2.1
numpy>=1.23.5
openai>=1.0.0

@ -3,6 +3,37 @@
{% block title %}Result View{% endblock %}
{% block style %}
<style>
.llm-advice-content {
font-size: 0.95rem;
color: #333;
}
.llm-advice-content h5, .llm-advice-content h6 {
color: #2c3e50;
border-left: 4px solid #3498db;
padding-left: 10px;
margin-bottom: 15px;
}
.llm-advice-content p {
margin-bottom: 12px;
text-align: justify;
}
.llm-advice-content li {
margin-bottom: 5px;
}
.llm-advice-content strong {
color: #e74c3c;
}
#llm-advice-container .alert {
border-radius: 8px;
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content %}
@ -130,6 +161,24 @@
投組季報酬率
</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">正在生成AI投資建議...</p>
</div>
</div>
<button id="refresh-advice" class="btn btn-outline-primary btn-sm mt-2" onclick="refreshLLMAdvice()">
🔄 重新生成建議
</button>
</div>
</div>
</div>
</div>
</div>
@ -190,5 +239,108 @@
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();
});
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">正在生成AI投資建議...</p>
</div>
`;
// 調用API
const response = await fetch(`/api/llm_advice/${strategyId}`);
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 formatLLMAdvice(advice) {
// 將LLM回應格式化為HTML
const lines = advice.split('\n');
let html = '';
for (let line of lines) {
line = line.trim();
if (!line) {
html += '<br>';
continue;
}
// 標題處理
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('•')) {
html += `<li>${line.substring(1).trim()}</li>`;
}
// 數字列表
else if (line.match(/^\d+\./)) {
html += `<li>${line}</li>`;
}
// 粗體文本
else if (line.includes('**') && line.split('**').length >= 3) {
const parts = line.split('**');
html += `<p>${parts[0]}<strong>${parts[1]}</strong>${parts[2]}</p>`;
}
// 普通段落
else {
html += `<p>${line}</p>`;
}
}
return `
<div class="llm-advice-content" style="line-height: 1.6;">
${html}
</div>
`;
}
</script>
{% endblock script %}

@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""
LLM 投資建議服務測試腳本
用於測試 LLM 服務是否正常工作
"""
import os
import sys
import json
from unittest.mock import Mock, patch
# 添加專案根目錄到 Python 路徑
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from llm_service import LLMInvestmentAdvisor, get_llm_advisor
from prompts.investment_advice import get_comprehensive_analysis_prompt
def test_llm_service_initialization():
"""測試 LLM 服務初始化"""
print("🔍 測試 LLM 服務初始化...")
try:
# 測試沒有 API 金鑰的情況
try:
advisor = LLMInvestmentAdvisor(api_key=None)
print("❌ 應該拋出錯誤但沒有拋出")
return False
except ValueError as e:
print(f"✅ 正確拋出錯誤:{e}")
# 測試有假 API 金鑰的情況
fake_api_key = "sk-fake-key-for-testing"
try:
advisor = LLMInvestmentAdvisor(api_key=fake_api_key)
print("✅ 成功初始化 LLM 服務")
return True
except Exception as e:
print(f"❌ 初始化失敗:{e}")
return False
except Exception as e:
print(f"❌ 測試失敗:{e}")
return False
def test_prompt_generation():
"""測試 Prompt 生成"""
print("\n🔍 測試 Prompt 生成...")
try:
# 模擬策略資料
mock_strategy_data = {
'id': 123,
'name': '測試策略',
'role': '最大化夏普比率',
'annual_ret': 0.15,
'vol': 0.20,
'annual_sr': 1.8,
'mdd': -0.12,
'alpha': 0.08,
'beta': 0.95,
'var10': 0.05,
'r2': 0.85,
'gamma': 2.5,
'assets': ['AAPL', 'GOOGL', 'MSFT'],
'tw': True
}
prompt = get_comprehensive_analysis_prompt(mock_strategy_data)
if len(prompt) > 100 and '策略名稱:測試策略' in prompt:
print("✅ Prompt 生成成功")
print(f"📝 Prompt 長度:{len(prompt)} 字符")
return True
else:
print("❌ Prompt 生成格式錯誤")
return False
except Exception as e:
print(f"❌ Prompt 生成失敗:{e}")
return False
def test_fallback_advice():
"""測試 Fallback 建議"""
print("\n🔍 測試 Fallback 建議...")
try:
advisor = LLMInvestmentAdvisor(api_key="fake-key")
# 模擬會失敗的策略資料
strategy_data = {
'annual_ret': 0.25,
'vol': 0.15,
'annual_sr': 2.1
}
fallback_advice = advisor._get_fallback_advice(strategy_data)
if len(fallback_advice) > 100 and '年化報酬率:25.00%' in fallback_advice:
print("✅ Fallback 建議生成成功")
return True
else:
print("❌ Fallback 建議格式錯誤")
return False
except Exception as e:
print(f"❌ Fallback 測試失敗:{e}")
return False
def test_api_format():
"""測試 API 回應格式"""
print("\n🔍 測試 API 回應格式...")
try:
# 模擬成功的 API 回應
mock_response = {
'success': True,
'advice': '這是一個測試建議。年化報酬率表現良好,建議持續監控市場變化。',
'strategy_id': 123
}
if all(key in mock_response for key in ['success', 'advice', 'strategy_id']):
print("✅ API 回應格式正確")
return True
else:
print("❌ API 回應格式錯誤")
return False
except Exception as e:
print(f"❌ API 格式測試失敗:{e}")
return False
def test_error_format():
"""測試錯誤回應格式"""
print("\n🔍 測試錯誤回應格式...")
try:
# 模擬錯誤回應
mock_error_response = {
'success': False,
'error': 'API 金鑰無效',
'details': '請檢查 OpenAI API 金鑰設定'
}
if all(key in mock_error_response for key in ['success', 'error']):
print("✅ 錯誤回應格式正確")
return True
else:
print("❌ 錯誤回應格式錯誤")
return False
except Exception as e:
print(f"❌ 錯誤格式測試失敗:{e}")
return False
def main():
"""主測試函數"""
print("🚀 開始測試 LLM 投資建議服務\n")
tests = [
("LLM 服務初始化", test_llm_service_initialization),
("Prompt 生成", test_prompt_generation),
("Fallback 建議", test_fallback_advice),
("API 回應格式", test_api_format),
("錯誤回應格式", test_error_format),
]
passed = 0
total = len(tests)
for test_name, test_func in tests:
if test_func():
passed += 1
print()
print("📊 測試結果摘要" print(f"✅ 通過:{passed}/{total}")
print(f"❌ 失敗:{total - passed}/{total}")
if passed == total:
print("🎉 所有測試通過!LLM 服務準備就緒。")
return 0
else:
print(" 部分測試失敗,請檢查配置。")
return 1
if __name__ == "__main__":
exit(main())
Loading…
Cancel
Save