Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -124,241 +124,6 @@ class StockAnalyzer:
|
|
| 124 |
'symbol': symbol
|
| 125 |
}
|
| 126 |
|
| 127 |
-
def get_fundamental_data(self, symbol):
|
| 128 |
-
"""獲取基本面數據"""
|
| 129 |
-
try:
|
| 130 |
-
ticker = yf.Ticker(symbol)
|
| 131 |
-
info = ticker.info
|
| 132 |
-
|
| 133 |
-
# 獲取財務數據
|
| 134 |
-
fundamental_data = {
|
| 135 |
-
'pe_ratio': info.get('trailingPE', None), # 本益比
|
| 136 |
-
'peg_ratio': info.get('pegRatio', None), # PEG比率
|
| 137 |
-
'pb_ratio': info.get('priceToBook', None), # 股價淨值比
|
| 138 |
-
'dividend_yield': info.get('dividendYield', 0) * 100 if info.get('dividendYield') else 0, # 股息殖利率
|
| 139 |
-
'roe': info.get('returnOnEquity', None), # 股東權益報酬率
|
| 140 |
-
'debt_to_equity': info.get('debtToEquity', None), # 負債權益比
|
| 141 |
-
'current_ratio': info.get('currentRatio', None), # 流動比率
|
| 142 |
-
'gross_margins': info.get('grossMargins', None), # 毛利率
|
| 143 |
-
'operating_margins': info.get('operatingMargins', None), # 營業利益率
|
| 144 |
-
'profit_margins': info.get('profitMargins', None), # 淨利率
|
| 145 |
-
'revenue_growth': info.get('revenueGrowth', None), # 營收成長率
|
| 146 |
-
'earnings_growth': info.get('earningsGrowth', None), # 盈餘成長率
|
| 147 |
-
'book_value': info.get('bookValue', None), # 每股淨值
|
| 148 |
-
'eps': info.get('trailingEps', None), # 每股盈餘
|
| 149 |
-
'forward_eps': info.get('forwardEps', None), # 預估每股盈餘
|
| 150 |
-
'market_cap': info.get('marketCap', None), # 市值
|
| 151 |
-
'enterprise_value': info.get('enterpriseValue', None), # 企業價值
|
| 152 |
-
'price_to_sales': info.get('priceToSalesTrailing12Months', None), # 股價營收比
|
| 153 |
-
'beta': info.get('beta', None), # 貝塔值
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
return fundamental_data
|
| 157 |
-
|
| 158 |
-
except Exception as e:
|
| 159 |
-
print(f"基本面數據獲取失敗 {symbol}: {str(e)}")
|
| 160 |
-
return {}
|
| 161 |
-
|
| 162 |
-
def calculate_fair_value(self, symbol, fundamental_data, current_price):
|
| 163 |
-
"""計算合理價位"""
|
| 164 |
-
try:
|
| 165 |
-
fair_values = []
|
| 166 |
-
methods_used = []
|
| 167 |
-
|
| 168 |
-
# 方法1: 本益比法 (PE Method)
|
| 169 |
-
if fundamental_data.get('eps') and fundamental_data.get('pe_ratio'):
|
| 170 |
-
industry_avg_pe = 15 # 假設產業平均本益比
|
| 171 |
-
pe_fair_value = fundamental_data['eps'] * industry_avg_pe
|
| 172 |
-
fair_values.append(pe_fair_value)
|
| 173 |
-
methods_used.append('本益比法')
|
| 174 |
-
|
| 175 |
-
# 方法2: 淨值比法 (PB Method)
|
| 176 |
-
if fundamental_data.get('book_value') and fundamental_data.get('pb_ratio'):
|
| 177 |
-
industry_avg_pb = 1.5 # 假設產業平均淨值比
|
| 178 |
-
pb_fair_value = fundamental_data['book_value'] * industry_avg_pb
|
| 179 |
-
fair_values.append(pb_fair_value)
|
| 180 |
-
methods_used.append('淨值比法')
|
| 181 |
-
|
| 182 |
-
# 方法3: 股息折現模型 (DDM)
|
| 183 |
-
if fundamental_data.get('dividend_yield') and fundamental_data['dividend_yield'] > 0:
|
| 184 |
-
required_return = 0.08 # 假設要求報酬率8%
|
| 185 |
-
dividend_per_share = current_price * (fundamental_data['dividend_yield'] / 100)
|
| 186 |
-
growth_rate = fundamental_data.get('earnings_growth', 0.03) # 假設3%成長率
|
| 187 |
-
if required_return > growth_rate:
|
| 188 |
-
ddm_fair_value = dividend_per_share * (1 + growth_rate) / (required_return - growth_rate)
|
| 189 |
-
fair_values.append(ddm_fair_value)
|
| 190 |
-
methods_used.append('股息折現法')
|
| 191 |
-
|
| 192 |
-
# 方法4: 自由現金流折現 (簡化版)
|
| 193 |
-
if fundamental_data.get('market_cap') and fundamental_data.get('profit_margins'):
|
| 194 |
-
# 簡化的DCF估算
|
| 195 |
-
estimated_fcf = fundamental_data['market_cap'] * (fundamental_data['profit_margins'] or 0.05)
|
| 196 |
-
discount_rate = 0.1 # 10%折現率
|
| 197 |
-
growth_rate = 0.03 # 3%永續成長率
|
| 198 |
-
terminal_value = estimated_fcf * (1 + growth_rate) / (discount_rate - growth_rate)
|
| 199 |
-
shares_outstanding = fundamental_data['market_cap'] / current_price if current_price else 1
|
| 200 |
-
dcf_fair_value = terminal_value / shares_outstanding
|
| 201 |
-
fair_values.append(dcf_fair_value)
|
| 202 |
-
methods_used.append('現金流折現法')
|
| 203 |
-
|
| 204 |
-
# 計算平均合理價位
|
| 205 |
-
if fair_values:
|
| 206 |
-
average_fair_value = sum(fair_values) / len(fair_values)
|
| 207 |
-
return {
|
| 208 |
-
'fair_value': average_fair_value,
|
| 209 |
-
'methods_used': methods_used,
|
| 210 |
-
'individual_values': dict(zip(methods_used, fair_values)),
|
| 211 |
-
'upside_potential': ((average_fair_value - current_price) / current_price * 100) if current_price else 0
|
| 212 |
-
}
|
| 213 |
-
else:
|
| 214 |
-
return {
|
| 215 |
-
'fair_value': None,
|
| 216 |
-
'methods_used': [],
|
| 217 |
-
'individual_values': {},
|
| 218 |
-
'upside_potential': 0
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
except Exception as e:
|
| 222 |
-
print(f"合理價位計算失敗 {symbol}: {str(e)}")
|
| 223 |
-
return {
|
| 224 |
-
'fair_value': None,
|
| 225 |
-
'methods_used': [],
|
| 226 |
-
'individual_values': {},
|
| 227 |
-
'upside_potential': 0
|
| 228 |
-
}
|
| 229 |
-
|
| 230 |
-
def generate_investment_recommendation(self, symbol, current_price, technical_probs, fair_value_data, fundamental_data):
|
| 231 |
-
"""生成投資建議"""
|
| 232 |
-
try:
|
| 233 |
-
recommendations = []
|
| 234 |
-
overall_score = 0
|
| 235 |
-
max_score = 100
|
| 236 |
-
|
| 237 |
-
# 技術面評分 (30%)
|
| 238 |
-
tech_score = 0
|
| 239 |
-
if technical_probs['up'] > 50:
|
| 240 |
-
tech_score = 25
|
| 241 |
-
recommendations.append("✅ 技術面: 上漲機率較高,技術指標偏多")
|
| 242 |
-
elif technical_probs['down'] > 50:
|
| 243 |
-
tech_score = 5
|
| 244 |
-
recommendations.append("❌ 技術面: 下跌機率較高,技術指標偏空")
|
| 245 |
-
else:
|
| 246 |
-
tech_score = 15
|
| 247 |
-
recommendations.append("⚠️ 技術面: 盤整機率較高,缺乏明確方向")
|
| 248 |
-
|
| 249 |
-
# 基本面評分 (40%)
|
| 250 |
-
fundamental_score = 0
|
| 251 |
-
|
| 252 |
-
# PE比評估
|
| 253 |
-
pe_ratio = fundamental_data.get('pe_ratio')
|
| 254 |
-
if pe_ratio:
|
| 255 |
-
if pe_ratio < 15:
|
| 256 |
-
fundamental_score += 8
|
| 257 |
-
recommendations.append(f"✅ 本益比: {pe_ratio:.1f} (合理偏低)")
|
| 258 |
-
elif pe_ratio > 25:
|
| 259 |
-
fundamental_score += 2
|
| 260 |
-
recommendations.append(f"⚠️ 本益比: {pe_ratio:.1f} (偏高)")
|
| 261 |
-
else:
|
| 262 |
-
fundamental_score += 5
|
| 263 |
-
recommendations.append(f"✅ 本益比: {pe_ratio:.1f} (合理)")
|
| 264 |
-
|
| 265 |
-
# ROE評估
|
| 266 |
-
roe = fundamental_data.get('roe')
|
| 267 |
-
if roe:
|
| 268 |
-
if roe > 0.15:
|
| 269 |
-
fundamental_score += 8
|
| 270 |
-
recommendations.append(f"✅ ROE: {roe*100:.1f}% (優秀)")
|
| 271 |
-
elif roe > 0.10:
|
| 272 |
-
fundamental_score += 5
|
| 273 |
-
recommendations.append(f"✅ ROE: {roe*100:.1f}% (良好)")
|
| 274 |
-
else:
|
| 275 |
-
fundamental_score += 2
|
| 276 |
-
recommendations.append(f"⚠️ ROE: {roe*100:.1f}% (偏低)")
|
| 277 |
-
|
| 278 |
-
# 負債比評估
|
| 279 |
-
debt_ratio = fundamental_data.get('debt_to_equity')
|
| 280 |
-
if debt_ratio is not None:
|
| 281 |
-
if debt_ratio < 0.5:
|
| 282 |
-
fundamental_score += 6
|
| 283 |
-
recommendations.append(f"✅ 負債比: {debt_ratio:.2f} (健康)")
|
| 284 |
-
elif debt_ratio < 1.0:
|
| 285 |
-
fundamental_score += 3
|
| 286 |
-
recommendations.append(f"⚠️ 負債比: {debt_ratio:.2f} (適中)")
|
| 287 |
-
else:
|
| 288 |
-
fundamental_score += 1
|
| 289 |
-
recommendations.append(f"❌ 負債比: {debt_ratio:.2f} (偏高)")
|
| 290 |
-
|
| 291 |
-
# 成長性評估
|
| 292 |
-
revenue_growth = fundamental_data.get('revenue_growth')
|
| 293 |
-
if revenue_growth:
|
| 294 |
-
if revenue_growth > 0.1:
|
| 295 |
-
fundamental_score += 8
|
| 296 |
-
recommendations.append(f"✅ 營收成長: {revenue_growth*100:.1f}% (強勁)")
|
| 297 |
-
elif revenue_growth > 0:
|
| 298 |
-
fundamental_score += 5
|
| 299 |
-
recommendations.append(f"✅ 營收成長: {revenue_growth*100:.1f}% (正成長)")
|
| 300 |
-
else:
|
| 301 |
-
fundamental_score += 1
|
| 302 |
-
recommendations.append(f"❌ 營收成長: {revenue_growth*100:.1f}% (負成長)")
|
| 303 |
-
|
| 304 |
-
# 合理價位評估 (30%)
|
| 305 |
-
valuation_score = 0
|
| 306 |
-
if fair_value_data['fair_value'] and current_price:
|
| 307 |
-
upside = fair_value_data['upside_potential']
|
| 308 |
-
if upside > 20:
|
| 309 |
-
valuation_score = 25
|
| 310 |
-
recommendations.append(f"✅ 估值: 現價相對合理價位有{upside:.1f}%上漲空間")
|
| 311 |
-
elif upside > 0:
|
| 312 |
-
valuation_score = 20
|
| 313 |
-
recommendations.append(f"✅ 估值: 現價相對合理價位有{upside:.1f}%上漲空間")
|
| 314 |
-
elif upside > -10:
|
| 315 |
-
valuation_score = 15
|
| 316 |
-
recommendations.append(f"⚠️ 估值: 現價接近合理價位 ({upside:+.1f}%)")
|
| 317 |
-
else:
|
| 318 |
-
valuation_score = 5
|
| 319 |
-
recommendations.append(f"❌ 估值: 現價高於合理價位{abs(upside):.1f}%")
|
| 320 |
-
|
| 321 |
-
overall_score = tech_score + fundamental_score + valuation_score
|
| 322 |
-
|
| 323 |
-
# 生成最終建議
|
| 324 |
-
if overall_score >= 70:
|
| 325 |
-
final_recommendation = "🚀 強力買入"
|
| 326 |
-
action_color = "green"
|
| 327 |
-
elif overall_score >= 55:
|
| 328 |
-
final_recommendation = "✅ 買入"
|
| 329 |
-
action_color = "lightgreen"
|
| 330 |
-
elif overall_score >= 40:
|
| 331 |
-
final_recommendation = "⚠️ 觀望"
|
| 332 |
-
action_color = "orange"
|
| 333 |
-
elif overall_score >= 25:
|
| 334 |
-
final_recommendation = "❌ 避開"
|
| 335 |
-
action_color = "lightcoral"
|
| 336 |
-
else:
|
| 337 |
-
final_recommendation = "🔻 賣出"
|
| 338 |
-
action_color = "red"
|
| 339 |
-
|
| 340 |
-
return {
|
| 341 |
-
'overall_score': overall_score,
|
| 342 |
-
'max_score': max_score,
|
| 343 |
-
'final_recommendation': final_recommendation,
|
| 344 |
-
'action_color': action_color,
|
| 345 |
-
'detailed_analysis': recommendations,
|
| 346 |
-
'fair_value': fair_value_data['fair_value'],
|
| 347 |
-
'upside_potential': fair_value_data['upside_potential']
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
-
except Exception as e:
|
| 351 |
-
print(f"投資建議生成失敗 {symbol}: {str(e)}")
|
| 352 |
-
return {
|
| 353 |
-
'overall_score': 50,
|
| 354 |
-
'max_score': 100,
|
| 355 |
-
'final_recommendation': "⚠️ 資料不足",
|
| 356 |
-
'action_color': "gray",
|
| 357 |
-
'detailed_analysis': ["資料不足,無法提供完整分析"],
|
| 358 |
-
'fair_value': None,
|
| 359 |
-
'upside_potential': 0
|
| 360 |
-
}
|
| 361 |
-
|
| 362 |
def calculate_technical_indicators(self):
|
| 363 |
"""計算技術指標"""
|
| 364 |
if self.data is None:
|
|
@@ -578,27 +343,20 @@ class StockAnalyzer:
|
|
| 578 |
|
| 579 |
report = f"""
|
| 580 |
## 📊 {self.symbol} AI 分析報告
|
| 581 |
-
|
| 582 |
### 📈 技術面分析:
|
| 583 |
{chr(10).join(f"• {signal}" for signal in technical_signals)}
|
| 584 |
-
|
| 585 |
### 💭 市場情感:{sentiment}
|
| 586 |
-
|
| 587 |
### 📊 近期表現:
|
| 588 |
- 5日漲跌幅:{price_change:+.2f}%
|
| 589 |
- 當前價位:${recent_data['Close'].iloc[-1]:.2f}
|
| 590 |
-
|
| 591 |
### 🤖 AI 預測機率(短期 1-7天):
|
| 592 |
-
|
| 593 |
| 方向 | 機率 | 說明 |
|
| 594 |
|------|------|------|
|
| 595 |
| 📈 **上漲** | **{probabilities['up']:.1f}%** | 股價向上突破的可能性 |
|
| 596 |
| 📉 **下跌** | **{probabilities['down']:.1f}%** | 股價向下修正的可能性 |
|
| 597 |
| ➡️ **盤整** | **{probabilities['sideways']:.1f}%** | 股價維持震盪的可能性 |
|
| 598 |
-
|
| 599 |
### 🎯 主要預測方向:
|
| 600 |
{direction_emoji} **{main_direction}** ({confidence_desc} - {confidence*100:.0f}%)
|
| 601 |
-
|
| 602 |
### 📋 投資建議:
|
| 603 |
"""
|
| 604 |
|
|
@@ -620,15 +378,12 @@ class StockAnalyzer:
|
|
| 620 |
- 🛡️ **風險管理**:小部位測試,嚴格執行停損"""
|
| 621 |
|
| 622 |
report += f"""
|
| 623 |
-
|
| 624 |
### 📅 中期展望(1個月):
|
| 625 |
基於當前技術面和市場情緒分析,建議持續關注:
|
| 626 |
- 關鍵技術位:支撐與阻力區間
|
| 627 |
- 市場情緒變化:新聞面和資金流向
|
| 628 |
- 整體大盤走勢:系統性風險評估
|
| 629 |
-
|
| 630 |
⚠️ **風險提醒**:此分析基於歷史數據和 AI 模型預測,僅供參考。投資有風險,請謹慎評估並做好風險管理!
|
| 631 |
-
|
| 632 |
---
|
| 633 |
*預測信心度:{confidence*100:.0f}% | 分析時間:{datetime.now().strftime('%Y-%m-%d %H:%M')}*
|
| 634 |
"""
|
|
@@ -738,20 +493,18 @@ def create_results_table(results):
|
|
| 738 |
# 創建表格 HTML
|
| 739 |
table_html = """
|
| 740 |
<div style="overflow-x: auto; margin: 20px 0;">
|
| 741 |
-
<table style="width: 100%; border-collapse: collapse; font-family: Arial, sans-serif;
|
| 742 |
<thead>
|
| 743 |
<tr style="background-color: #f0f0f0;">
|
| 744 |
-
<th style="border: 1px solid #ddd; padding:
|
| 745 |
-
<th style="border: 1px solid #ddd; padding:
|
| 746 |
-
<th style="border: 1px solid #ddd; padding:
|
| 747 |
-
<th style="border: 1px solid #ddd; padding:
|
| 748 |
-
<th style="border: 1px solid #ddd; padding:
|
| 749 |
-
<th style="border: 1px solid #ddd; padding:
|
| 750 |
-
<th style="border: 1px solid #ddd; padding:
|
| 751 |
-
<th style="border: 1px solid #ddd; padding:
|
| 752 |
-
<th style="border: 1px solid #ddd; padding:
|
| 753 |
-
<th style="border: 1px solid #ddd; padding: 8px; text-align: center;">技術面</th>
|
| 754 |
-
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">狀態</th>
|
| 755 |
</tr>
|
| 756 |
</thead>
|
| 757 |
<tbody>
|
|
@@ -762,50 +515,34 @@ def create_results_table(results):
|
|
| 762 |
if result['error_message']:
|
| 763 |
direction = "❌ 錯誤"
|
| 764 |
row_color = "#fff2f2"
|
| 765 |
-
tech_direction = "❌ 錯誤"
|
| 766 |
else:
|
| 767 |
up_prob = float(result['up_probability'])
|
| 768 |
down_prob = float(result['down_probability'])
|
| 769 |
sideways_prob = float(result['sideways_probability'])
|
| 770 |
|
| 771 |
-
# 技術面方向
|
| 772 |
if up_prob > down_prob and up_prob > sideways_prob:
|
| 773 |
-
|
|
|
|
| 774 |
elif down_prob > up_prob and down_prob > sideways_prob:
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
tech_direction = "➡️ 盤整"
|
| 778 |
-
|
| 779 |
-
# 根據投資建議決定背景顏色
|
| 780 |
-
recommendation = result.get('recommendation', '')
|
| 781 |
-
if '強力買入' in recommendation or '買入' in recommendation:
|
| 782 |
-
row_color = "#e8f5e8" # 淡綠色
|
| 783 |
-
elif '觀望' in recommendation:
|
| 784 |
-
row_color = "#fff8dc" # 淡黃色
|
| 785 |
-
elif '避開' in recommendation or '賣出' in recommendation:
|
| 786 |
-
row_color = "#ffe4e1" # 淡紅色
|
| 787 |
else:
|
|
|
|
| 788 |
row_color = "#f8f8f8" # 淡灰色
|
| 789 |
|
| 790 |
-
status = "✅ 成功" if not result['error_message'] else f"❌ {result['error_message'][:
|
| 791 |
-
|
| 792 |
-
# 投資建議樣式
|
| 793 |
-
rec_color = result.get('recommendation_color', 'gray')
|
| 794 |
-
investment_rec = f"<span style='color: {rec_color}; font-weight: bold;'>{result.get('recommendation', 'N/A')}</span>"
|
| 795 |
|
| 796 |
table_html += f"""
|
| 797 |
<tr style="background-color: {row_color};">
|
| 798 |
-
<td style="border: 1px solid #ddd; padding:
|
| 799 |
-
<td style="border: 1px solid #ddd; padding:
|
| 800 |
-
<td style="border: 1px solid #ddd; padding:
|
| 801 |
-
<td style="border: 1px solid #ddd; padding:
|
| 802 |
-
<td style="border: 1px solid #ddd; padding:
|
| 803 |
-
<td style="border: 1px solid #ddd; padding:
|
| 804 |
-
<td style="border: 1px solid #ddd; padding:
|
| 805 |
-
<td style="border: 1px solid #ddd; padding:
|
| 806 |
-
<td style="border: 1px solid #ddd; padding:
|
| 807 |
-
<td style="border: 1px solid #ddd; padding: 6px; text-align: center;">{tech_direction}</td>
|
| 808 |
-
<td style="border: 1px solid #ddd; padding: 6px; font-size: 10px;">{status}</td>
|
| 809 |
</tr>
|
| 810 |
"""
|
| 811 |
|
|
@@ -817,81 +554,6 @@ def create_results_table(results):
|
|
| 817 |
|
| 818 |
return table_html
|
| 819 |
|
| 820 |
-
def create_detailed_analysis_report(results):
|
| 821 |
-
"""創建詳細分析報告"""
|
| 822 |
-
if not results:
|
| 823 |
-
return ""
|
| 824 |
-
|
| 825 |
-
# 過濾成功的結果
|
| 826 |
-
success_results = [r for r in results if r['error_message'] == '']
|
| 827 |
-
|
| 828 |
-
if not success_results:
|
| 829 |
-
return "<p>沒有成功分析的股票可顯示詳細報告。</p>"
|
| 830 |
-
|
| 831 |
-
report_html = """
|
| 832 |
-
<div style="font-family: Arial, sans-serif; margin: 20px 0;">
|
| 833 |
-
"""
|
| 834 |
-
|
| 835 |
-
for result in success_results[:5]: # 只顯示前5個成功的結果
|
| 836 |
-
symbol = result['symbol']
|
| 837 |
-
name = result['name']
|
| 838 |
-
recommendation = result.get('recommendation', 'N/A')
|
| 839 |
-
rec_color = result.get('recommendation_color', 'gray')
|
| 840 |
-
score = result.get('investment_score', 'N/A')
|
| 841 |
-
detailed_analysis = result.get('detailed_analysis', [])
|
| 842 |
-
|
| 843 |
-
report_html += f"""
|
| 844 |
-
<div style="border: 2px solid #ddd; border-radius: 10px; padding: 15px; margin: 15px 0; background-color: #fafafa;">
|
| 845 |
-
<h3 style="color: #333; margin: 0 0 10px 0;">
|
| 846 |
-
📈 {symbol} - {name}
|
| 847 |
-
<span style="float: right; color: {rec_color}; font-weight: bold;">{recommendation}</span>
|
| 848 |
-
</h3>
|
| 849 |
-
|
| 850 |
-
<div style="background: linear-gradient(90deg, #f0f0f0 0%, #e0e0e0 {score}%, #f0f0f0 100%);
|
| 851 |
-
border-radius: 5px; padding: 5px; margin: 10px 0;">
|
| 852 |
-
<strong>綜合評分: {score}/100</strong>
|
| 853 |
-
</div>
|
| 854 |
-
|
| 855 |
-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 15px 0;">
|
| 856 |
-
<div>
|
| 857 |
-
<h4 style="color: #2e7d32; margin: 5px 0;">📊 基本資訊</h4>
|
| 858 |
-
<ul style="margin: 5px 0; padding-left: 20px;">
|
| 859 |
-
<li>現價: {result['current_price']}</li>
|
| 860 |
-
<li>合理價: {result.get('fair_value', 'N/A')}</li>
|
| 861 |
-
<li>上漲空間: {result.get('upside_potential', 'N/A')}</li>
|
| 862 |
-
</ul>
|
| 863 |
-
</div>
|
| 864 |
-
<div>
|
| 865 |
-
<h4 style="color: #1976d2; margin: 5px 0;">📈 財務指標</h4>
|
| 866 |
-
<ul style="margin: 5px 0; padding-left: 20px;">
|
| 867 |
-
<li>本益比: {result.get('pe_ratio', 'N/A')}</li>
|
| 868 |
-
<li>ROE: {result.get('roe', 'N/A')}</li>
|
| 869 |
-
<li>技術面: {result['up_probability']}%↑ {result['down_probability']}%↓</li>
|
| 870 |
-
</ul>
|
| 871 |
-
</div>
|
| 872 |
-
</div>
|
| 873 |
-
|
| 874 |
-
<div>
|
| 875 |
-
<h4 style="color: #f57c00; margin: 10px 0 5px 0;">🔍 詳細分析</h4>
|
| 876 |
-
<ul style="margin: 0; padding-left: 20px; line-height: 1.6;">
|
| 877 |
-
"""
|
| 878 |
-
|
| 879 |
-
for analysis_point in detailed_analysis:
|
| 880 |
-
report_html += f"<li>{analysis_point}</li>"
|
| 881 |
-
|
| 882 |
-
report_html += """
|
| 883 |
-
</ul>
|
| 884 |
-
</div>
|
| 885 |
-
</div>
|
| 886 |
-
"""
|
| 887 |
-
|
| 888 |
-
if len(success_results) > 5:
|
| 889 |
-
report_html += f"<p style='text-align: center; color: #666;'>... 還有 {len(success_results) - 5} 支股票的詳細分析結果</p>"
|
| 890 |
-
|
| 891 |
-
report_html += "</div>"
|
| 892 |
-
|
| 893 |
-
return report_html
|
| 894 |
-
|
| 895 |
def create_batch_analysis_charts(results):
|
| 896 |
"""創建批次分析結果圖表"""
|
| 897 |
if not results:
|
|
@@ -1025,7 +687,7 @@ def batch_analyze_stocks(stock_input_text):
|
|
| 1025 |
"""批次分析股票清單"""
|
| 1026 |
# 檢查輸入是否為空
|
| 1027 |
if not stock_input_text or not stock_input_text.strip():
|
| 1028 |
-
return "❌ 請輸入股票代號!", "", None, None, None, None, ""
|
| 1029 |
|
| 1030 |
try:
|
| 1031 |
# 從文字輸入框解析股票清單
|
|
@@ -1035,7 +697,7 @@ def batch_analyze_stocks(stock_input_text):
|
|
| 1035 |
stock_symbols = [symbol.strip().upper() for symbol in stock_symbols if symbol.strip()]
|
| 1036 |
|
| 1037 |
if not stock_symbols:
|
| 1038 |
-
return "❌ 未能解析出有效的股票代號!", "", None, None, None, None, ""
|
| 1039 |
|
| 1040 |
# 準備結果列表
|
| 1041 |
results = []
|
|
@@ -1066,16 +728,8 @@ def batch_analyze_stocks(stock_input_text):
|
|
| 1066 |
'down_probability': 'ERROR',
|
| 1067 |
'sideways_probability': 'ERROR',
|
| 1068 |
'confidence': 'ERROR',
|
| 1069 |
-
'fair_value': 'N/A',
|
| 1070 |
-
'upside_potential': 'N/A',
|
| 1071 |
-
'investment_score': 'N/A',
|
| 1072 |
-
'recommendation': '❌ 錯誤',
|
| 1073 |
-
'recommendation_color': 'red',
|
| 1074 |
-
'pe_ratio': 'N/A',
|
| 1075 |
-
'roe': 'N/A',
|
| 1076 |
'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 1077 |
-
'error_message': message
|
| 1078 |
-
'detailed_analysis': []
|
| 1079 |
})
|
| 1080 |
progress_messages.append(f"❌ {symbol}: {message}")
|
| 1081 |
continue
|
|
@@ -1091,16 +745,8 @@ def batch_analyze_stocks(stock_input_text):
|
|
| 1091 |
'down_probability': 'ERROR',
|
| 1092 |
'sideways_probability': 'ERROR',
|
| 1093 |
'confidence': 'ERROR',
|
| 1094 |
-
'fair_value': 'N/A',
|
| 1095 |
-
'upside_potential': 'N/A',
|
| 1096 |
-
'investment_score': 'N/A',
|
| 1097 |
-
'recommendation': '❌ 數據不足',
|
| 1098 |
-
'recommendation_color': 'red',
|
| 1099 |
-
'pe_ratio': 'N/A',
|
| 1100 |
-
'roe': 'N/A',
|
| 1101 |
'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 1102 |
-
'error_message': '數據不足,無法分析'
|
| 1103 |
-
'detailed_analysis': []
|
| 1104 |
})
|
| 1105 |
progress_messages.append(f"❌ {symbol}: 數據不足")
|
| 1106 |
continue
|
|
@@ -1126,38 +772,18 @@ def batch_analyze_stocks(stock_input_text):
|
|
| 1126 |
|
| 1127 |
# 獲取股票資訊
|
| 1128 |
stock_info = analyzer.get_stock_info(symbol)
|
| 1129 |
-
current_price = latest['Close']
|
| 1130 |
-
|
| 1131 |
-
# 獲取基本面數據
|
| 1132 |
-
fundamental_data = analyzer.get_fundamental_data(symbol)
|
| 1133 |
-
|
| 1134 |
-
# 計算合理價位
|
| 1135 |
-
fair_value_data = analyzer.calculate_fair_value(symbol, fundamental_data, current_price)
|
| 1136 |
-
|
| 1137 |
-
# 生成投資建議
|
| 1138 |
-
investment_rec = analyzer.generate_investment_recommendation(
|
| 1139 |
-
symbol, current_price, probabilities, fair_value_data, fundamental_data
|
| 1140 |
-
)
|
| 1141 |
|
| 1142 |
# 記錄成功結果
|
| 1143 |
results.append({
|
| 1144 |
'symbol': symbol,
|
| 1145 |
'name': stock_info['name'],
|
| 1146 |
-
'current_price': f"{
|
| 1147 |
'up_probability': f"{probabilities['up']:.1f}",
|
| 1148 |
'down_probability': f"{probabilities['down']:.1f}",
|
| 1149 |
'sideways_probability': f"{probabilities['sideways']:.1f}",
|
| 1150 |
'confidence': f"{probabilities['confidence']*100:.1f}",
|
| 1151 |
-
'fair_value': f"{fair_value_data['fair_value']:.2f}" if fair_value_data['fair_value'] else 'N/A',
|
| 1152 |
-
'upside_potential': f"{fair_value_data['upside_potential']:+.1f}%" if fair_value_data['upside_potential'] else 'N/A',
|
| 1153 |
-
'investment_score': f"{investment_rec['overall_score']:.0f}",
|
| 1154 |
-
'recommendation': investment_rec['final_recommendation'],
|
| 1155 |
-
'recommendation_color': investment_rec['action_color'],
|
| 1156 |
-
'pe_ratio': f"{fundamental_data.get('pe_ratio', 0):.1f}" if fundamental_data.get('pe_ratio') else 'N/A',
|
| 1157 |
-
'roe': f"{fundamental_data.get('roe', 0)*100:.1f}%" if fundamental_data.get('roe') else 'N/A',
|
| 1158 |
'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 1159 |
-
'error_message': ''
|
| 1160 |
-
'detailed_analysis': investment_rec['detailed_analysis']
|
| 1161 |
})
|
| 1162 |
|
| 1163 |
progress_messages.append(f"✅ {symbol}: 分析完成")
|
|
@@ -1172,16 +798,8 @@ def batch_analyze_stocks(stock_input_text):
|
|
| 1172 |
'down_probability': 'ERROR',
|
| 1173 |
'sideways_probability': 'ERROR',
|
| 1174 |
'confidence': 'ERROR',
|
| 1175 |
-
'fair_value': 'N/A',
|
| 1176 |
-
'upside_potential': 'N/A',
|
| 1177 |
-
'investment_score': 'N/A',
|
| 1178 |
-
'recommendation': '❌ 系統錯誤',
|
| 1179 |
-
'recommendation_color': 'red',
|
| 1180 |
-
'pe_ratio': 'N/A',
|
| 1181 |
-
'roe': 'N/A',
|
| 1182 |
'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 1183 |
-
'error_message': f'未預期錯誤: {str(e)}'
|
| 1184 |
-
'detailed_analysis': []
|
| 1185 |
})
|
| 1186 |
progress_messages.append(f"❌ {symbol}: 未預期錯誤")
|
| 1187 |
|
|
@@ -1191,18 +809,15 @@ def batch_analyze_stocks(stock_input_text):
|
|
| 1191 |
|
| 1192 |
summary_message = f"""
|
| 1193 |
📈 批次分析完成!
|
| 1194 |
-
|
| 1195 |
📊 **分析統計:**
|
| 1196 |
- 總計股票數:{len(stock_symbols)}
|
| 1197 |
- 成功分析:{success_count}
|
| 1198 |
- 分析失敗:{error_count}
|
| 1199 |
-
|
| 1200 |
� **圖表已生成:**
|
| 1201 |
- 📊 機率比較柱狀圖
|
| 1202 |
- 🎯 信心度散佈圖
|
| 1203 |
- 📈 綜合評分雷達圖
|
| 1204 |
- 🥧 市場情緒餅圖
|
| 1205 |
-
|
| 1206 |
🎯 **請查看下方圖表進行投資決策分析!**
|
| 1207 |
"""
|
| 1208 |
|
|
@@ -1211,12 +826,11 @@ def batch_analyze_stocks(stock_input_text):
|
|
| 1211 |
# 創建圖表和結果表格
|
| 1212 |
chart_bar, chart_scatter, chart_radar, chart_pie = create_batch_analysis_charts(results)
|
| 1213 |
results_table = create_results_table(results)
|
| 1214 |
-
detailed_report = create_detailed_analysis_report(results)
|
| 1215 |
|
| 1216 |
-
return summary_message, progress_log, chart_bar, chart_scatter, chart_radar, chart_pie, results_table
|
| 1217 |
|
| 1218 |
except Exception as e:
|
| 1219 |
-
return f"❌ 批次分析過程中發生錯誤:{str(e)}", "", None, None, None, None, ""
|
| 1220 |
|
| 1221 |
# 創建 Gradio 界面
|
| 1222 |
with gr.Blocks(title="AI 股票分析師", theme=gr.themes.Soft()) as app:
|
|
@@ -1306,16 +920,12 @@ with gr.Blocks(title="AI 股票分析師", theme=gr.themes.Soft()) as app:
|
|
| 1306 |
stock_input_batch = gr.Textbox(
|
| 1307 |
label="📝 股票代號清單",
|
| 1308 |
placeholder="""請輸入多個股票代號,支援多種分隔方式:
|
| 1309 |
-
|
| 1310 |
• 換行分隔:
|
| 1311 |
2330.TW
|
| 1312 |
2317.TW
|
| 1313 |
1303.TW
|
| 1314 |
-
|
| 1315 |
• 逗號分隔:2330.TW, 2317.TW, 1303.TW
|
| 1316 |
-
|
| 1317 |
• 空格分隔:2330.TW 2317.TW 1303.TW
|
| 1318 |
-
|
| 1319 |
• 混合分隔:2330.TW, 2317.TW
|
| 1320 |
1303.TW; AAPL TSLA""",
|
| 1321 |
lines=8,
|
|
@@ -1373,12 +983,8 @@ with gr.Blocks(title="AI 股票分析師", theme=gr.themes.Soft()) as app:
|
|
| 1373 |
chart_sentiment = gr.Plot(label="🥧 整體市場情緒分佈")
|
| 1374 |
|
| 1375 |
# 詳細結果表格
|
| 1376 |
-
gr.Markdown("## 📋
|
| 1377 |
-
results_table = gr.HTML(label="
|
| 1378 |
-
|
| 1379 |
-
# 詳細分析報告
|
| 1380 |
-
gr.Markdown("## 📊 個股詳細分析")
|
| 1381 |
-
detailed_analysis = gr.HTML(label="詳細投資分析報告")
|
| 1382 |
|
| 1383 |
# 快速範例按鈕事件綁定
|
| 1384 |
example_tw_btn.click(
|
|
@@ -1412,8 +1018,7 @@ with gr.Blocks(title="AI 股票分析師", theme=gr.themes.Soft()) as app:
|
|
| 1412 |
chart_confidence,
|
| 1413 |
chart_radar,
|
| 1414 |
chart_sentiment,
|
| 1415 |
-
results_table
|
| 1416 |
-
detailed_analysis
|
| 1417 |
]
|
| 1418 |
)
|
| 1419 |
|
|
|
|
| 124 |
'symbol': symbol
|
| 125 |
}
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
def calculate_technical_indicators(self):
|
| 128 |
"""計算技術指標"""
|
| 129 |
if self.data is None:
|
|
|
|
| 343 |
|
| 344 |
report = f"""
|
| 345 |
## 📊 {self.symbol} AI 分析報告
|
|
|
|
| 346 |
### 📈 技術面分析:
|
| 347 |
{chr(10).join(f"• {signal}" for signal in technical_signals)}
|
|
|
|
| 348 |
### 💭 市場情感:{sentiment}
|
|
|
|
| 349 |
### 📊 近期表現:
|
| 350 |
- 5日漲跌幅:{price_change:+.2f}%
|
| 351 |
- 當前價位:${recent_data['Close'].iloc[-1]:.2f}
|
|
|
|
| 352 |
### 🤖 AI 預測機率(短期 1-7天):
|
|
|
|
| 353 |
| 方向 | 機率 | 說明 |
|
| 354 |
|------|------|------|
|
| 355 |
| 📈 **上漲** | **{probabilities['up']:.1f}%** | 股價向上突破的可能性 |
|
| 356 |
| 📉 **下跌** | **{probabilities['down']:.1f}%** | 股價向下修正的可能性 |
|
| 357 |
| ➡️ **盤整** | **{probabilities['sideways']:.1f}%** | 股價維持震盪的可能性 |
|
|
|
|
| 358 |
### 🎯 主要預測方向:
|
| 359 |
{direction_emoji} **{main_direction}** ({confidence_desc} - {confidence*100:.0f}%)
|
|
|
|
| 360 |
### 📋 投資建議:
|
| 361 |
"""
|
| 362 |
|
|
|
|
| 378 |
- 🛡️ **風險管理**:小部位測試,嚴格執行停損"""
|
| 379 |
|
| 380 |
report += f"""
|
|
|
|
| 381 |
### 📅 中期展望(1個月):
|
| 382 |
基於當前技術面和市場情緒分析,建議持續關注:
|
| 383 |
- 關鍵技術位:支撐與阻力區間
|
| 384 |
- 市場情緒變化:新聞面和資金流向
|
| 385 |
- 整體大盤走勢:系統性風險評估
|
|
|
|
| 386 |
⚠️ **風險提醒**:此分析基於歷史數據和 AI 模型預測,僅供參考。投資有風險,請謹慎評估並做好風險管理!
|
|
|
|
| 387 |
---
|
| 388 |
*預測信心度:{confidence*100:.0f}% | 分析時間:{datetime.now().strftime('%Y-%m-%d %H:%M')}*
|
| 389 |
"""
|
|
|
|
| 493 |
# 創建表格 HTML
|
| 494 |
table_html = """
|
| 495 |
<div style="overflow-x: auto; margin: 20px 0;">
|
| 496 |
+
<table style="width: 100%; border-collapse: collapse; font-family: Arial, sans-serif;">
|
| 497 |
<thead>
|
| 498 |
<tr style="background-color: #f0f0f0;">
|
| 499 |
+
<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">股票代號</th>
|
| 500 |
+
<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">股票名稱</th>
|
| 501 |
+
<th style="border: 1px solid #ddd; padding: 12px; text-align: right;">當前價格</th>
|
| 502 |
+
<th style="border: 1px solid #ddd; padding: 12px; text-align: right;">上漲機率(%)</th>
|
| 503 |
+
<th style="border: 1px solid #ddd; padding: 12px; text-align: right;">下跌機率(%)</th>
|
| 504 |
+
<th style="border: 1px solid #ddd; padding: 12px; text-align: right;">盤整機率(%)</th>
|
| 505 |
+
<th style="border: 1px solid #ddd; padding: 12px; text-align: right;">信心度(%)</th>
|
| 506 |
+
<th style="border: 1px solid #ddd; padding: 12px; text-align: center;">預測方向</th>
|
| 507 |
+
<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">狀態</th>
|
|
|
|
|
|
|
| 508 |
</tr>
|
| 509 |
</thead>
|
| 510 |
<tbody>
|
|
|
|
| 515 |
if result['error_message']:
|
| 516 |
direction = "❌ 錯誤"
|
| 517 |
row_color = "#fff2f2"
|
|
|
|
| 518 |
else:
|
| 519 |
up_prob = float(result['up_probability'])
|
| 520 |
down_prob = float(result['down_probability'])
|
| 521 |
sideways_prob = float(result['sideways_probability'])
|
| 522 |
|
|
|
|
| 523 |
if up_prob > down_prob and up_prob > sideways_prob:
|
| 524 |
+
direction = "📈 看多"
|
| 525 |
+
row_color = "#f0fff0" # 淡綠色
|
| 526 |
elif down_prob > up_prob and down_prob > sideways_prob:
|
| 527 |
+
direction = "📉 看空"
|
| 528 |
+
row_color = "#fff0f0" # 淡紅色
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
else:
|
| 530 |
+
direction = "➡️ 盤整"
|
| 531 |
row_color = "#f8f8f8" # 淡灰色
|
| 532 |
|
| 533 |
+
status = "✅ 成功" if not result['error_message'] else f"❌ {result['error_message'][:30]}..."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
table_html += f"""
|
| 536 |
<tr style="background-color: {row_color};">
|
| 537 |
+
<td style="border: 1px solid #ddd; padding: 8px; font-weight: bold;">{result['symbol']}</td>
|
| 538 |
+
<td style="border: 1px solid #ddd; padding: 8px;">{result['name']}</td>
|
| 539 |
+
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['current_price']}</td>
|
| 540 |
+
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['up_probability']}</td>
|
| 541 |
+
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['down_probability']}</td>
|
| 542 |
+
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['sideways_probability']}</td>
|
| 543 |
+
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['confidence']}</td>
|
| 544 |
+
<td style="border: 1px solid #ddd; padding: 8px; text-align: center;">{direction}</td>
|
| 545 |
+
<td style="border: 1px solid #ddd; padding: 8px;">{status}</td>
|
|
|
|
|
|
|
| 546 |
</tr>
|
| 547 |
"""
|
| 548 |
|
|
|
|
| 554 |
|
| 555 |
return table_html
|
| 556 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
def create_batch_analysis_charts(results):
|
| 558 |
"""創建批次分析結果圖表"""
|
| 559 |
if not results:
|
|
|
|
| 687 |
"""批次分析股票清單"""
|
| 688 |
# 檢查輸入是否為空
|
| 689 |
if not stock_input_text or not stock_input_text.strip():
|
| 690 |
+
return "❌ 請輸入股票代號!", "", None, None, None, None, ""
|
| 691 |
|
| 692 |
try:
|
| 693 |
# 從文字輸入框解析股票清單
|
|
|
|
| 697 |
stock_symbols = [symbol.strip().upper() for symbol in stock_symbols if symbol.strip()]
|
| 698 |
|
| 699 |
if not stock_symbols:
|
| 700 |
+
return "❌ 未能解析出有效的股票代號!", "", None, None, None, None, ""
|
| 701 |
|
| 702 |
# 準備結果列表
|
| 703 |
results = []
|
|
|
|
| 728 |
'down_probability': 'ERROR',
|
| 729 |
'sideways_probability': 'ERROR',
|
| 730 |
'confidence': 'ERROR',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 732 |
+
'error_message': message
|
|
|
|
| 733 |
})
|
| 734 |
progress_messages.append(f"❌ {symbol}: {message}")
|
| 735 |
continue
|
|
|
|
| 745 |
'down_probability': 'ERROR',
|
| 746 |
'sideways_probability': 'ERROR',
|
| 747 |
'confidence': 'ERROR',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 749 |
+
'error_message': '數據不足,無法分析'
|
|
|
|
| 750 |
})
|
| 751 |
progress_messages.append(f"❌ {symbol}: 數據不足")
|
| 752 |
continue
|
|
|
|
| 772 |
|
| 773 |
# 獲取股票資訊
|
| 774 |
stock_info = analyzer.get_stock_info(symbol)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
|
| 776 |
# 記錄成功結果
|
| 777 |
results.append({
|
| 778 |
'symbol': symbol,
|
| 779 |
'name': stock_info['name'],
|
| 780 |
+
'current_price': f"{latest['Close']:.2f}" if latest['Close'] else 'N/A',
|
| 781 |
'up_probability': f"{probabilities['up']:.1f}",
|
| 782 |
'down_probability': f"{probabilities['down']:.1f}",
|
| 783 |
'sideways_probability': f"{probabilities['sideways']:.1f}",
|
| 784 |
'confidence': f"{probabilities['confidence']*100:.1f}",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 785 |
'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 786 |
+
'error_message': ''
|
|
|
|
| 787 |
})
|
| 788 |
|
| 789 |
progress_messages.append(f"✅ {symbol}: 分析完成")
|
|
|
|
| 798 |
'down_probability': 'ERROR',
|
| 799 |
'sideways_probability': 'ERROR',
|
| 800 |
'confidence': 'ERROR',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 802 |
+
'error_message': f'未預期錯誤: {str(e)}'
|
|
|
|
| 803 |
})
|
| 804 |
progress_messages.append(f"❌ {symbol}: 未預期錯誤")
|
| 805 |
|
|
|
|
| 809 |
|
| 810 |
summary_message = f"""
|
| 811 |
📈 批次分析完成!
|
|
|
|
| 812 |
📊 **分析統計:**
|
| 813 |
- 總計股票數:{len(stock_symbols)}
|
| 814 |
- 成功分析:{success_count}
|
| 815 |
- 分析失敗:{error_count}
|
|
|
|
| 816 |
� **圖表已生成:**
|
| 817 |
- 📊 機率比較柱狀圖
|
| 818 |
- 🎯 信心度散佈圖
|
| 819 |
- 📈 綜合評分雷達圖
|
| 820 |
- 🥧 市場情緒餅圖
|
|
|
|
| 821 |
🎯 **請查看下方圖表進行投資決策分析!**
|
| 822 |
"""
|
| 823 |
|
|
|
|
| 826 |
# 創建圖表和結果表格
|
| 827 |
chart_bar, chart_scatter, chart_radar, chart_pie = create_batch_analysis_charts(results)
|
| 828 |
results_table = create_results_table(results)
|
|
|
|
| 829 |
|
| 830 |
+
return summary_message, progress_log, chart_bar, chart_scatter, chart_radar, chart_pie, results_table
|
| 831 |
|
| 832 |
except Exception as e:
|
| 833 |
+
return f"❌ 批次分析過程中發生錯誤:{str(e)}", "", None, None, None, None, ""
|
| 834 |
|
| 835 |
# 創建 Gradio 界面
|
| 836 |
with gr.Blocks(title="AI 股票分析師", theme=gr.themes.Soft()) as app:
|
|
|
|
| 920 |
stock_input_batch = gr.Textbox(
|
| 921 |
label="📝 股票代號清單",
|
| 922 |
placeholder="""請輸入多個股票代號,支援多種分隔方式:
|
|
|
|
| 923 |
• 換行分隔:
|
| 924 |
2330.TW
|
| 925 |
2317.TW
|
| 926 |
1303.TW
|
|
|
|
| 927 |
• 逗號分隔:2330.TW, 2317.TW, 1303.TW
|
|
|
|
| 928 |
• 空格分隔:2330.TW 2317.TW 1303.TW
|
|
|
|
| 929 |
• 混合分隔:2330.TW, 2317.TW
|
| 930 |
1303.TW; AAPL TSLA""",
|
| 931 |
lines=8,
|
|
|
|
| 983 |
chart_sentiment = gr.Plot(label="🥧 整體市場情緒分佈")
|
| 984 |
|
| 985 |
# 詳細結果表格
|
| 986 |
+
gr.Markdown("## 📋 詳細分析結果")
|
| 987 |
+
results_table = gr.HTML(label="分析結果表格")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 988 |
|
| 989 |
# 快速範例按鈕事件綁定
|
| 990 |
example_tw_btn.click(
|
|
|
|
| 1018 |
chart_confidence,
|
| 1019 |
chart_radar,
|
| 1020 |
chart_sentiment,
|
| 1021 |
+
results_table
|
|
|
|
| 1022 |
]
|
| 1023 |
)
|
| 1024 |
|