54justin commited on
Commit
d314e22
·
verified ·
1 Parent(s): 692fc19

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1378 -0
app.py ADDED
@@ -0,0 +1,1378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 由 Copilot 生成 - AI 股票分析師 (含批次分析功能)
2
+ import subprocess
3
+ import sys
4
+ import os
5
+ from datetime import datetime
6
+
7
+ # 環境檢測
8
+ IS_HUGGINGFACE_SPACE = "SPACE_ID" in os.environ
9
+ print(f"運行環境: {'Hugging Face Spaces' if IS_HUGGINGFACE_SPACE else '本地環境'}")
10
+
11
+ # 檢查並安裝所需套件的函數
12
+ def install_package(package_name):
13
+ try:
14
+ __import__(package_name)
15
+ except ImportError:
16
+ print(f"正在安裝 {package_name}...")
17
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
18
+
19
+ # 安裝必要套件
20
+ required_packages = [
21
+ "torch>=2.0.0",
22
+ "torchvision>=0.15.0",
23
+ "torchaudio>=2.0.0",
24
+ "yfinance>=0.2.18",
25
+ "gradio>=4.0.0",
26
+ "pandas>=1.5.0",
27
+ "numpy>=1.21.0",
28
+ "matplotlib>=3.5.0",
29
+ "plotly>=5.0.0",
30
+ "beautifulsoup4>=4.11.0",
31
+ "requests>=2.28.0",
32
+ "transformers>=4.21.0",
33
+ "accelerate>=0.20.0",
34
+ "tokenizers>=0.13.0"
35
+ ]
36
+
37
+ for package in required_packages:
38
+ package_name = package.split(">=")[0].split("==")[0]
39
+ if package_name == "beautifulsoup4":
40
+ package_name = "bs4"
41
+ try:
42
+ __import__(package_name)
43
+ except ImportError:
44
+ print(f"正在安裝 {package}...")
45
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package])
46
+
47
+ # 現在導入所有套件
48
+ import gradio as gr
49
+ import yfinance as yf
50
+ import pandas as pd
51
+ import numpy as np
52
+ import matplotlib.pyplot as plt
53
+ import plotly.graph_objects as go
54
+ import plotly.express as px
55
+ from datetime import datetime, timedelta
56
+ import requests
57
+ from bs4 import BeautifulSoup
58
+ from transformers import pipeline
59
+ import warnings
60
+ warnings.filterwarnings('ignore')
61
+
62
+ # 初始化 Hugging Face 模型
63
+ print("正在載入 AI 模型...")
64
+
65
+ # 嘗試載入模型,如果失敗則使用較輕量的替代方案
66
+ try:
67
+ sentiment_analyzer = pipeline("sentiment-analysis", model="ProsusAI/finbert")
68
+ print("FinBERT 情感分析模型載入成功")
69
+ except Exception as e:
70
+ print(f"FinBERT 載入失敗,嘗試替代模型: {e}")
71
+ try:
72
+ sentiment_analyzer = pipeline("sentiment-analysis", model="nlptown/bert-base-multilingual-uncased-sentiment")
73
+ print("多語言情感分析模型載入成功")
74
+ except Exception as e2:
75
+ print(f"替代模型載入失敗: {e2}")
76
+ sentiment_analyzer = None
77
+
78
+ try:
79
+ summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
80
+ print("BART 摘要模型載入成功")
81
+ except Exception as e:
82
+ print(f"BART 載入失敗,嘗試替代模型: {e}")
83
+ try:
84
+ summarizer = pipeline("summarization", model="sshleifer/distilbart-cnn-12-6")
85
+ print("DistilBART 摘要模型載入成功")
86
+ except Exception as e2:
87
+ print(f"摘要模型載入失敗: {e2}")
88
+ summarizer = None
89
+
90
+ class StockAnalyzer:
91
+ def __init__(self):
92
+ self.data = None
93
+ self.symbol = None
94
+
95
+ def fetch_stock_data(self, symbol, period="1y"):
96
+ """獲取股票歷史數據"""
97
+ try:
98
+ ticker = yf.Ticker(symbol)
99
+ self.data = ticker.history(period=period)
100
+ self.symbol = symbol
101
+ # 獲取股票資訊
102
+ info = ticker.info
103
+ stock_name = info.get('longName', info.get('shortName', symbol))
104
+ return True, f"成功獲取 {symbol} 的歷史數據", stock_name
105
+ except Exception as e:
106
+ return False, f"數據獲取失敗: {str(e)}", None
107
+
108
+ def get_stock_info(self, symbol):
109
+ """獲取股票基本資訊"""
110
+ try:
111
+ ticker = yf.Ticker(symbol)
112
+ info = ticker.info
113
+ current_price = self.data['Close'].iloc[-1] if self.data is not None else None
114
+ stock_name = info.get('longName', info.get('shortName', symbol))
115
+ return {
116
+ 'name': stock_name,
117
+ 'current_price': current_price,
118
+ 'symbol': symbol
119
+ }
120
+ except Exception as e:
121
+ return {
122
+ 'name': symbol,
123
+ 'current_price': None,
124
+ 'symbol': symbol
125
+ }
126
+
127
+ def calculate_technical_indicators(self):
128
+ """計算技術指標"""
129
+ if self.data is None:
130
+ return None
131
+
132
+ df = self.data.copy()
133
+
134
+ # 移動平均線
135
+ df['MA5'] = df['Close'].rolling(window=5).mean()
136
+ df['MA20'] = df['Close'].rolling(window=20).mean()
137
+ df['MA60'] = df['Close'].rolling(window=60).mean()
138
+
139
+ # RSI 相對強弱指標
140
+ delta = df['Close'].diff()
141
+ gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
142
+ loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
143
+ rs = gain / loss
144
+ df['RSI'] = 100 - (100 / (1 + rs))
145
+
146
+ # MACD
147
+ exp1 = df['Close'].ewm(span=12).mean()
148
+ exp2 = df['Close'].ewm(span=26).mean()
149
+ df['MACD'] = exp1 - exp2
150
+ df['MACD_signal'] = df['MACD'].ewm(span=9).mean()
151
+
152
+ # 布林通道
153
+ df['BB_middle'] = df['Close'].rolling(window=20).mean()
154
+ bb_std = df['Close'].rolling(window=20).std()
155
+ df['BB_upper'] = df['BB_middle'] + (bb_std * 2)
156
+ df['BB_lower'] = df['BB_middle'] - (bb_std * 2)
157
+
158
+ return df
159
+
160
+ def get_news_sentiment(self, symbol):
161
+ """獲取並分析新聞情感"""
162
+ try:
163
+ # 模擬新聞標題(實際應用中需要接入新聞 API)
164
+ sample_news = [
165
+ f"{symbol} 股價創新高,投資人信心大增",
166
+ f"市場關注 {symbol} 最新財報表現",
167
+ f"{symbol} 面臨供應鏈挑戰,股價承壓",
168
+ f"分析師上調 {symbol} 目標價,看好後市",
169
+ f"{symbol} 技術創新獲得市場認可"
170
+ ]
171
+
172
+ sentiments = []
173
+
174
+ # 檢查情感分析模型是否可用
175
+ if sentiment_analyzer is None:
176
+ # 如果模型不可用,返回模擬的情感分析結果
177
+ for news in sample_news:
178
+ # 簡單的關鍵詞情感分析替代方案
179
+ positive_words = ['創新高', '信心大增', '上調', '看好', '創新', '獲得認可']
180
+ negative_words = ['挑戰', '承壓', '面臨', '下滑']
181
+
182
+ score = 0.5 # 中性
183
+ sentiment = 'NEUTRAL'
184
+
185
+ for word in positive_words:
186
+ if word in news:
187
+ score = 0.8
188
+ sentiment = 'POSITIVE'
189
+ break
190
+
191
+ for word in negative_words:
192
+ if word in news:
193
+ score = 0.8
194
+ sentiment = 'NEGATIVE'
195
+ break
196
+
197
+ sentiments.append({
198
+ 'text': news,
199
+ 'sentiment': sentiment,
200
+ 'score': score
201
+ })
202
+ else:
203
+ # 使用 AI 模型進行情感分析
204
+ for news in sample_news:
205
+ result = sentiment_analyzer(news)[0]
206
+ sentiments.append({
207
+ 'text': news,
208
+ 'sentiment': result['label'],
209
+ 'score': result['score']
210
+ })
211
+
212
+ return sentiments
213
+
214
+ except Exception as e:
215
+ return [{'text': f'新聞分析暫時無法使用: {str(e)}', 'sentiment': 'NEUTRAL', 'score': 0.5}]
216
+
217
+ def analyze_sentiment_summary(self, sentiments):
218
+ """分析情感摘要"""
219
+ if not sentiments:
220
+ return "中性"
221
+
222
+ positive_count = sum(1 for s in sentiments if s['sentiment'] == 'POSITIVE')
223
+ negative_count = sum(1 for s in sentiments if s['sentiment'] == 'NEGATIVE')
224
+
225
+ if positive_count > negative_count:
226
+ return "偏樂觀"
227
+ elif negative_count > positive_count:
228
+ return "偏悲觀"
229
+ else:
230
+ return "中性"
231
+
232
+ def calculate_prediction_probabilities(self, technical_signals, sentiment, recent_data):
233
+ """計算上漲和下跌機率"""
234
+ # 計算技術面得分
235
+ bullish_signals = sum(1 for signal in technical_signals if "多頭" in signal or "機會" in signal)
236
+ bearish_signals = sum(1 for signal in technical_signals if "空頭" in signal or "警訊" in signal)
237
+ neutral_signals = len(technical_signals) - bullish_signals - bearish_signals
238
+
239
+ # 技術面得分 (-1 到 1)
240
+ total_signals = len(technical_signals)
241
+ if total_signals > 0:
242
+ tech_score = (bullish_signals - bearish_signals) / total_signals
243
+ else:
244
+ tech_score = 0
245
+
246
+ # 情感得分 (-1 到 1)
247
+ sentiment_score = 0
248
+ if sentiment == "偏樂觀":
249
+ sentiment_score = 0.6
250
+ elif sentiment == "偏悲觀":
251
+ sentiment_score = -0.6
252
+ else:
253
+ sentiment_score = 0
254
+
255
+ # 價格動量得分
256
+ price_change = ((recent_data['Close'].iloc[-1] - recent_data['Close'].iloc[-5]) / recent_data['Close'].iloc[-5]) * 100
257
+ momentum_score = np.tanh(price_change / 10) # 標準化到 -1 到 1
258
+
259
+ # RSI 得分
260
+ latest = recent_data.iloc[-1]
261
+ rsi = latest.get('RSI', 50)
262
+ if rsi > 70:
263
+ rsi_score = -0.5 # 超買,偏空
264
+ elif rsi < 30:
265
+ rsi_score = 0.5 # 超賣,偏多
266
+ else:
267
+ rsi_score = (50 - rsi) / 100 # 標準化
268
+
269
+ # MACD 得分
270
+ macd_score = 0
271
+ if 'MACD' in latest and 'MACD_signal' in latest:
272
+ if latest['MACD'] > latest['MACD_signal']:
273
+ macd_score = 0.3
274
+ else:
275
+ macd_score = -0.3
276
+
277
+ # 綜合得分計算(加權平均)
278
+ weights = {
279
+ 'tech': 0.25,
280
+ 'sentiment': 0.20,
281
+ 'momentum': 0.25,
282
+ 'rsi': 0.15,
283
+ 'macd': 0.15
284
+ }
285
+
286
+ total_score = (
287
+ tech_score * weights['tech'] +
288
+ sentiment_score * weights['sentiment'] +
289
+ momentum_score * weights['momentum'] +
290
+ rsi_score * weights['rsi'] +
291
+ macd_score * weights['macd']
292
+ )
293
+
294
+ # 將得分轉換為機率 (使用 sigmoid 函數)
295
+ def sigmoid(x):
296
+ return 1 / (1 + np.exp(-x * 3)) # 放大 3 倍讓機率更明顯
297
+
298
+ up_probability = sigmoid(total_score) * 100
299
+ down_probability = sigmoid(-total_score) * 100
300
+ sideways_probability = 100 - up_probability - down_probability
301
+
302
+ # 確保機率總和為 100%
303
+ total_prob = up_probability + down_probability + sideways_probability
304
+ up_probability = (up_probability / total_prob) * 100
305
+ down_probability = (down_probability / total_prob) * 100
306
+ sideways_probability = (sideways_probability / total_prob) * 100
307
+
308
+ return {
309
+ 'up': max(15, min(75, up_probability)), # 限制在 15%-75% 範圍內
310
+ 'down': max(15, min(75, down_probability)), # 限制在 15%-75% 範圍內
311
+ 'sideways': max(10, sideways_probability), # 至少 10%
312
+ 'confidence': abs(total_score) # 信心度
313
+ }
314
+
315
+ def generate_buy_sell_recommendation(self, probabilities, confidence, recent_data):
316
+ """生成具體的買賣建議 - 由 Copilot 生成"""
317
+ up_prob = probabilities['up']
318
+ down_prob = probabilities['down']
319
+ sideways_prob = probabilities['sideways']
320
+
321
+ # 當前價格和近期波動
322
+ current_price = recent_data['Close'].iloc[-1]
323
+ rsi = recent_data.iloc[-1].get('RSI', 50)
324
+
325
+ # 計算綜合評分
326
+ composite_score = up_prob * confidence
327
+
328
+ recommendations = {
329
+ 'holding_advice': "", # 持有股票時的建議
330
+ 'non_holding_advice': "", # 未持有時的建議
331
+ 'risk_level': "", # 風險等級
332
+ 'action_priority': "", # 操作優先級
333
+ 'position_sizing': "" # 倉位建議
334
+ }
335
+
336
+ # 根據機率和信心度決定建議
337
+ if up_prob >= 60 and confidence >= 0.3:
338
+ # 強烈看多
339
+ recommendations['holding_advice'] = "🔥 **強烈建議持有**:股價有很高機率上漲,建議繼續持有並可考慮加碼。設定止損點在當前價格下方5-8%。"
340
+ recommendations['non_holding_advice'] = "🚀 **積極買進**:建議分批進場,可先買入30-50%預期倉位,若回檔至支撐位再加碼。"
341
+ recommendations['risk_level'] = "中等風險"
342
+ recommendations['action_priority'] = "高優先級 - 建議行動"
343
+ recommendations['position_sizing'] = "建議倉位:60-80%"
344
+
345
+ elif up_prob >= 45 and up_prob < 60 and confidence >= 0.25:
346
+ # 溫和看多
347
+ recommendations['holding_advice'] = "✅ **建議持有**:維持現有持股,若股價突破關鍵阻力位可考慮小幅加碼。設定止損在-8%。"
348
+ recommendations['non_holding_advice'] = "💰 **適度買進**:可小量進場試單,建議先買入20-30%預期倉位,觀察後續走勢。"
349
+ recommendations['risk_level'] = "中等風險"
350
+ recommendations['action_priority'] = "中優先級 - 可考慮行動"
351
+ recommendations['position_sizing'] = "建議倉位:40-60%"
352
+
353
+ elif down_prob >= 60 and confidence >= 0.3:
354
+ # 強烈看空
355
+ recommendations['holding_advice'] = "🚨 **建議賣出**:股價下跌機率很高,建議減持50-80%持股,保留核心持股並嚴格設置止損。"
356
+ recommendations['non_holding_advice'] = "⛔ **強烈不建議買進**:市場風險較大,建議等待更佳進場時機,保持現金或考慮避險資產。"
357
+ recommendations['risk_level'] = "高風險"
358
+ recommendations['action_priority'] = "高優先級 - 建議防守"
359
+ recommendations['position_sizing'] = "建議倉位:10-20%"
360
+
361
+ elif down_prob >= 45 and down_prob < 60 and confidence >= 0.25:
362
+ # 溫和看空
363
+ recommendations['holding_advice'] = "⚠️ **謹慎持有**:可減持部分持股降低風險,保留核心持股,密切關注支撐位是否守住。"
364
+ recommendations['non_holding_advice'] = "🔍 **暫緩買進**:建議等待股價跌至更好的買點,或等待市場情緒好轉後再進場。"
365
+ recommendations['risk_level'] = "中高風險"
366
+ recommendations['action_priority'] = "中優先級 - 傾向防守"
367
+ recommendations['position_sizing'] = "建議倉位:20-40%"
368
+
369
+ else:
370
+ # 不確定/盤整
371
+ if confidence < 0.2:
372
+ # 低信心度
373
+ recommendations['holding_advice'] = "🤔 **觀望持有**:AI預測信心度較低,建議維持現狀,等待更明確的訊號。"
374
+ recommendations['non_holding_advice'] = "⏳ **保持觀望**:市場方向不明,建議等待更清晰的進場訊號,避免盲目進場。"
375
+ recommendations['risk_level'] = "不確定風險"
376
+ recommendations['action_priority'] = "低優先級 - 建議觀望"
377
+ recommendations['position_sizing'] = "建議倉位:維持現狀"
378
+ else:
379
+ # 中等信心度的盤整
380
+ recommendations['holding_advice'] = "📊 **區間持有**:股價可能在區間震蕩,可在高點減持、低點加碼,執行區間操作策略。"
381
+ recommendations['non_holding_advice'] = "🎯 **等待機會**:可在支撐位附近小量進場,等待突破訊號再決定是否加碼。"
382
+ recommendations['risk_level'] = "中等風險"
383
+ recommendations['action_priority'] = "中優先級 - 區間操作"
384
+ recommendations['position_sizing'] = "建議倉位:30-50%"
385
+
386
+ # RSI 超買超賣特殊建議
387
+ if rsi > 80:
388
+ recommendations['holding_advice'] += f"\n⚠️ **RSI超買警告**:RSI({rsi:.1f})顯示超買,注意短期回調風險。"
389
+ recommendations['non_holding_advice'] += f"\n⚠️ **等待回調**:RSI({rsi:.1f})超買,建議等待回調後再進場。"
390
+ elif rsi < 20:
391
+ recommendations['holding_advice'] += f"\n💎 **RSI超賣機會**:RSI({rsi:.1f})顯示超賣,可能有反彈機會。"
392
+ recommendations['non_holding_advice'] += f"\n💎 **超賣買點**:RSI({rsi:.1f})超賣,可能是不錯的買點。"
393
+
394
+ return recommendations
395
+
396
+ def check_alerts(self, recent_data, symbol):
397
+ """檢查四項 alert 功能 - 由 Copilot 生成"""
398
+ alerts = {
399
+ 'news_alert': {'status': 'N', 'message': '暫無重大公告'},
400
+ 'volume_alert': {'status': 'N', 'message': '量能正常'},
401
+ 'institutional_alert': {'status': 'N', 'message': '籌碼面正常'},
402
+ 'technical_alert': {'status': 'N', 'message': '技術面觀察中'}
403
+ }
404
+
405
+ try:
406
+ latest = recent_data.iloc[-1]
407
+
408
+ # 1. 新聞/公告 alert:公司重大公告(併購、BOT、合約、目標價上修)
409
+ # 模擬重大新聞檢查(實際應用中需要接入新聞 API)
410
+ import random
411
+ news_scenarios = [
412
+ f"{symbol} 宣布重大併購案,預計將擴大營運規模",
413
+ f"{symbol} 獲得政府 BOT 案標案,合約金額達數十億",
414
+ f"{symbol} 簽署重要合作協議,拓展海外市場",
415
+ f"分析師大幅上修 {symbol} 目標價,看好未來發展",
416
+ f"{symbol} 技術突破獲得認證,將帶動業績成長"
417
+ ]
418
+
419
+ # 隨機模擬是否有重大新聞(實際應用中應該從新聞 API 獲取)
420
+ if random.random() < 0.3: # 30% 機率有重大新聞
421
+ news_title = random.choice(news_scenarios)
422
+ alerts['news_alert'] = {
423
+ 'status': 'Y',
424
+ 'message': f'重大公告:{news_title}'
425
+ }
426
+
427
+ # 2. 量能 alert:當日量 >= 2×20日平均量
428
+ if 'Volume' in recent_data.columns:
429
+ current_volume = latest['Volume']
430
+ avg_20_volume = recent_data['Volume'].rolling(window=20).mean().iloc[-1]
431
+
432
+ if current_volume >= 2 * avg_20_volume:
433
+ # 檢查是否突破價格阻力
434
+ current_price = latest['Close']
435
+ resistance_level = recent_data['Close'].rolling(window=20).max().iloc[-2] # 前20日高點
436
+
437
+ if current_price > resistance_level:
438
+ alerts['volume_alert'] = {
439
+ 'status': 'Y',
440
+ 'message': f'量能爆發且突破阻力位(當日量:{current_volume:,.0f},20日均量:{avg_20_volume:,.0f})'
441
+ }
442
+ else:
443
+ alerts['volume_alert'] = {
444
+ 'status': 'N',
445
+ 'message': f'量能放大但未突破阻力位(當日量:{current_volume:,.0f},20日均量:{avg_20_volume:,.0f})'
446
+ }
447
+
448
+ # 3. 籌碼 alert:三大法人連續買超或外資當日淨買超↑
449
+ # 模擬三大法人買賣超資料(實際應用中需要接入相關 API)
450
+ consecutive_days = random.randint(1, 5)
451
+ foreign_net_buy = random.choice([True, False])
452
+
453
+ if consecutive_days >= 3 or foreign_net_buy:
454
+ if consecutive_days >= 3:
455
+ alerts['institutional_alert'] = {
456
+ 'status': 'Y',
457
+ 'message': f'三大法人連續 {consecutive_days} 日買超,籌碼面偏多'
458
+ }
459
+ else:
460
+ alerts['institutional_alert'] = {
461
+ 'status': 'Y',
462
+ 'message': '外資當日大幅淨買超,資金流入明顯'
463
+ }
464
+
465
+ # 4. 技術 alert:價格突破 20 日高且 MACD 交叉/OBV 上升
466
+ current_price = latest['Close']
467
+ ma20_high = recent_data['Close'].rolling(window=20).max().iloc[-2] # 前20日最高價
468
+
469
+ # 檢查 MACD 交叉
470
+ macd_crossover = False
471
+ if 'MACD' in latest and 'MACD_signal' in latest:
472
+ current_macd = latest['MACD']
473
+ current_signal = latest['MACD_signal']
474
+ prev_macd = recent_data['MACD'].iloc[-2] if len(recent_data) > 1 else current_macd
475
+ prev_signal = recent_data['MACD_signal'].iloc[-2] if len(recent_data) > 1 else current_signal
476
+
477
+ # 檢查黃金交叉
478
+ if current_macd > current_signal and prev_macd <= prev_signal:
479
+ macd_crossover = True
480
+
481
+ # 模擬 OBV 上升(實際應用中需要計算 OBV 指標)
482
+ obv_rising = random.choice([True, False])
483
+
484
+ if current_price > ma20_high and (macd_crossover or obv_rising):
485
+ technical_conditions = []
486
+ if macd_crossover:
487
+ technical_conditions.append("MACD 黃金交叉")
488
+ if obv_rising:
489
+ technical_conditions.append("OBV 上升")
490
+
491
+ alerts['technical_alert'] = {
492
+ 'status': 'Y',
493
+ 'message': f'突破 20 日高點且 {"/".join(technical_conditions)},考慮分批進場'
494
+ }
495
+
496
+ except Exception as e:
497
+ print(f"Alert 檢查發生錯誤: {e}")
498
+
499
+ return alerts
500
+
501
+ def generate_comprehensive_prediction(self, technical_signals, sentiment, recent_data):
502
+ """生成綜合預測報告 - 由 Copilot 生成"""
503
+ # 計算價格變化
504
+ price_change = ((recent_data['Close'].iloc[-1] - recent_data['Close'].iloc[-5]) / recent_data['Close'].iloc[-5]) * 100
505
+
506
+ # 計算預測機率
507
+ probabilities = self.calculate_prediction_probabilities(technical_signals, sentiment, recent_data)
508
+
509
+ # 確定主要預測方向
510
+ max_prob = max(probabilities['up'], probabilities['down'], probabilities['sideways'])
511
+ if probabilities['up'] == max_prob:
512
+ main_direction = "看多"
513
+ direction_emoji = "📈"
514
+ elif probabilities['down'] == max_prob:
515
+ main_direction = "看空"
516
+ direction_emoji = "📉"
517
+ else:
518
+ main_direction = "盤整"
519
+ direction_emoji = "➡️"
520
+
521
+ # 信心度描述
522
+ confidence = probabilities['confidence']
523
+ if confidence > 0.4:
524
+ confidence_desc = "高信心"
525
+ elif confidence > 0.2:
526
+ confidence_desc = "中等信心"
527
+ else:
528
+ confidence_desc = "低信心"
529
+
530
+ # 生成買賣建議
531
+ buy_sell_rec = self.generate_buy_sell_recommendation(probabilities, confidence, recent_data)
532
+
533
+ # 檢查四項 alert
534
+ alerts = self.check_alerts(recent_data, self.symbol)
535
+
536
+ report = f"""
537
+ ## 📊 {self.symbol} AI 分析報告
538
+
539
+ ### 🚨 重要 Alert 提醒:
540
+ #### 📰 新聞/公告 Alert:{alerts['news_alert']['status']}
541
+ {alerts['news_alert']['message']}
542
+
543
+ #### 📊 量能 Alert:{alerts['volume_alert']['status']}
544
+ {alerts['volume_alert']['message']}
545
+
546
+ #### 💰 籌碼 Alert:{alerts['institutional_alert']['status']}
547
+ {alerts['institutional_alert']['message']}
548
+
549
+ #### 📈 技術 Alert:{alerts['technical_alert']['status']}
550
+ {alerts['technical_alert']['message']}
551
+
552
+ ---
553
+ ### 📈 技術面分析:
554
+ {chr(10).join(f"• {signal}" for signal in technical_signals)}
555
+ ### 💭 市場情感:{sentiment}
556
+ ### 📊 近期表現:
557
+ - 5日漲跌幅:{price_change:+.2f}%
558
+ - 當前價位:${recent_data['Close'].iloc[-1]:.2f}
559
+ ### 🤖 AI 預測機率(短期 1-7天):
560
+ | 方向 | 機率 | 說明 |
561
+ |------|------|------|
562
+ | 📈 **上漲** | **{probabilities['up']:.1f}%** | 股價向上突破的可能性 |
563
+ | 📉 **下跌** | **{probabilities['down']:.1f}%** | 股價向下修正的可能性 |
564
+ | ➡️ **盤整** | **{probabilities['sideways']:.1f}%** | 股價維持震盪的可能性 |
565
+ ### 🎯 主要預測方向:
566
+ {direction_emoji} **{main_direction}** ({confidence_desc} - {confidence*100:.0f}%)
567
+
568
+ ### � **具體投資建議**
569
+ ---
570
+ #### 🏠 **若您目前持有此股票:**
571
+ {buy_sell_rec['holding_advice']}
572
+
573
+ #### 💰 **若您目前未持有此股票:**
574
+ {buy_sell_rec['non_holding_advice']}
575
+
576
+ #### 📊 **投資參數建議:**
577
+ - **風險等級:** {buy_sell_rec['risk_level']}
578
+ - **操作優先級:** {buy_sell_rec['action_priority']}
579
+ - **{buy_sell_rec['position_sizing']}**
580
+
581
+ ### 📋 一般性投資策略:
582
+ """
583
+
584
+ # 根據最高機率給出一般策略
585
+ if probabilities['up'] > 50:
586
+ report += """
587
+ - 💡 **多頭策略**:考慮逢低加碼或持有現有部位
588
+ - 🎯 **目標設定**:關注上方阻力位,設定合理獲利目標
589
+ - 🛡️ **風險管理**:設置止損點保護資本"""
590
+ elif probabilities['down'] > 50:
591
+ report += """
592
+ - 💡 **防守策略**:考慮減碼或等待更佳進場點
593
+ - 🎯 **支撐觀察**:留意下方支撐位是否守住
594
+ - 🛡️ **風險管理**:避免追高,控制倉位大小"""
595
+ else:
596
+ report += """
597
+ - 💡 **中性策略**:保持觀望,等待明確方向訊號
598
+ - 🎯 **區間操作**:可考慮在支撐阻力區間內操作
599
+ - 🛡️ **風險管理**:小部位測試,嚴格執行停損"""
600
+
601
+ report += f"""
602
+ ### 📅 中期展望(1個月):
603
+ 基於當前技術面和市場情緒分析,建議持續關注:
604
+ - 關鍵技術位:支撐與阻力區間
605
+ - 市場情緒變化:新聞面和資金流向
606
+ - 整體大盤走勢:系統性風險評估
607
+
608
+ ⚠️ **重要風險提醒**:
609
+ - 此分析基於歷史數據和 AI 模型預測,僅供參考
610
+ - 投資有風險,請謹慎評估並做好風險管理
611
+ - 建議結合個人財務狀況和投資目標做決策
612
+ - 請勿將此作為唯一投資依據
613
+ ---
614
+ *預測信心度:{confidence*100:.0f}% | 分析時間:{datetime.now().strftime('%Y-%m-%d %H:%M')} | 由 Copilot 生成*
615
+ """
616
+
617
+ return report
618
+
619
+ def generate_prediction(self, df, news_sentiment):
620
+ """生成預測分析"""
621
+ if df is None or len(df) < 30:
622
+ return "數據不足,無法進行預測分析"
623
+
624
+ # 獲取最新數據
625
+ latest = df.iloc[-1]
626
+ recent_data = df.tail(20)
627
+
628
+ # 技術分析信號
629
+ technical_signals = []
630
+
631
+ # 價格趋势
632
+ if latest['Close'] > latest['MA20']:
633
+ technical_signals.append("價格在20日均線之上(多頭信號)")
634
+ else:
635
+ technical_signals.append("價格在20日均線之下(空頭信號)")
636
+
637
+ # RSI 分析
638
+ rsi = latest['RSI']
639
+ if rsi > 70:
640
+ technical_signals.append(f"RSI({rsi:.1f}) 超買警訊")
641
+ elif rsi < 30:
642
+ technical_signals.append(f"RSI({rsi:.1f}) 超賣機會")
643
+ else:
644
+ technical_signals.append(f"RSI({rsi:.1f}) 正常範圍")
645
+
646
+ # MACD 分析
647
+ if latest['MACD'] > latest['MACD_signal']:
648
+ technical_signals.append("MACD 呈現多頭排列")
649
+ else:
650
+ technical_signals.append("MACD 呈現空頭排列")
651
+
652
+ # 新聞情感分析
653
+ sentiment_summary = self.analyze_sentiment_summary(news_sentiment)
654
+
655
+ # 綜合預測
656
+ prediction = self.generate_comprehensive_prediction(technical_signals, sentiment_summary, recent_data)
657
+
658
+ return prediction
659
+
660
+ # 創建分析器實例
661
+ analyzer = StockAnalyzer()
662
+
663
+ def analyze_stock(symbol):
664
+ """主要分析函數"""
665
+ if not symbol.strip():
666
+ return None, "請輸入股票代碼", ""
667
+
668
+ # 獲取數據
669
+ result = analyzer.fetch_stock_data(symbol.upper())
670
+ if len(result) == 3:
671
+ success, message, stock_name = result
672
+ else:
673
+ success, message = result
674
+ stock_name = None
675
+
676
+ if not success:
677
+ return None, message, ""
678
+
679
+ # 計算技術指標
680
+ df = analyzer.calculate_technical_indicators()
681
+
682
+ # 創建價格圖表
683
+ fig = go.Figure()
684
+
685
+ # 添加K線圖
686
+ fig.add_trace(go.Candlestick(
687
+ x=df.index,
688
+ open=df['Open'],
689
+ high=df['High'],
690
+ low=df['Low'],
691
+ close=df['Close'],
692
+ name='價格'
693
+ ))
694
+
695
+ # 添加移動平均線
696
+ fig.add_trace(go.Scatter(x=df.index, y=df['MA5'], name='MA5', line=dict(color='orange')))
697
+ fig.add_trace(go.Scatter(x=df.index, y=df['MA20'], name='MA20', line=dict(color='blue')))
698
+
699
+ fig.update_layout(
700
+ title=f'{symbol} 股價走勢與技術指標',
701
+ xaxis_title='日期',
702
+ yaxis_title='價格',
703
+ height=600
704
+ )
705
+
706
+ # 獲取新聞情感
707
+ news_sentiment = analyzer.get_news_sentiment(symbol)
708
+
709
+ # 生成預測
710
+ prediction = analyzer.generate_prediction(df, news_sentiment)
711
+
712
+ return fig, "分析完成!", prediction
713
+
714
+ def create_results_table(results):
715
+ """創建結果表格 - 由 Copilot 生成"""
716
+ if not results:
717
+ return ""
718
+
719
+ # 創建表格 HTML
720
+ table_html = """
721
+ <div style="overflow-x: auto; margin: 20px 0;">
722
+ <table style="width: 100%; border-collapse: collapse; font-family: Arial, sans-serif;">
723
+ <thead>
724
+ <tr style="background-color: #f0f0f0;">
725
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: left;">股票代號</th>
726
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: left;">股票名稱</th>
727
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: right;">當前價格</th>
728
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: right;">上漲機率(%)</th>
729
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: right;">下跌機率(%)</th>
730
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: right;">盤整機率(%)</th>
731
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: right;">信心度(%)</th>
732
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: center;">新聞Alert</th>
733
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: center;">量能Alert</th>
734
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: center;">籌碼Alert</th>
735
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: center;">技術Alert</th>
736
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: center;">預測方向</th>
737
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: center;">持股建議</th>
738
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: center;">買進建議</th>
739
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: left;">狀態</th>
740
+ </tr>
741
+ </thead>
742
+ <tbody>
743
+ """
744
+
745
+ for result in results:
746
+ # 判斷預測方向和顏色
747
+ if result['error_message']:
748
+ direction = "❌ 錯誤"
749
+ row_color = "#fff2f2"
750
+ holding_advice = "N/A"
751
+ buying_advice = "N/A"
752
+ news_alert = "N/A"
753
+ volume_alert = "N/A"
754
+ institutional_alert = "N/A"
755
+ technical_alert = "N/A"
756
+ else:
757
+ up_prob = float(result['up_probability'])
758
+ down_prob = float(result['down_probability'])
759
+ sideways_prob = float(result['sideways_probability'])
760
+ confidence = float(result['confidence']) / 100.0 # 轉換為小數
761
+
762
+ if up_prob > down_prob and up_prob > sideways_prob:
763
+ direction = "📈 看多"
764
+ row_color = "#f0fff0" # 淡綠色
765
+ elif down_prob > up_prob and down_prob > sideways_prob:
766
+ direction = "📉 看空"
767
+ row_color = "#fff0f0" # 淡紅色
768
+ else:
769
+ direction = "➡️ 盤整"
770
+ row_color = "#f8f8f8" # 淡灰色
771
+
772
+ # 生成簡化的買賣建議
773
+ if up_prob >= 60 and confidence >= 0.3:
774
+ holding_advice = "🔥 強烈持有"
775
+ buying_advice = "🚀 積極買進"
776
+ elif up_prob >= 45 and confidence >= 0.25:
777
+ holding_advice = "✅ 建議持有"
778
+ buying_advice = "💰 適度買進"
779
+ elif down_prob >= 60 and confidence >= 0.3:
780
+ holding_advice = "🚨 建議賣出"
781
+ buying_advice = "⛔ 不建議買進"
782
+ elif down_prob >= 45 and confidence >= 0.25:
783
+ holding_advice = "⚠️ 謹慎持有"
784
+ buying_advice = "🔍 暫緩買進"
785
+ else:
786
+ if confidence < 0.2:
787
+ holding_advice = "🤔 觀望持有"
788
+ buying_advice = "⏳ 保持觀望"
789
+ else:
790
+ holding_advice = "📊 區間持有"
791
+ buying_advice = "🎯 等待機會"
792
+
793
+ # Alert 狀態顯示
794
+ news_alert = f"{'🟢' if result.get('news_alert') == 'Y' else '🔴'} {result.get('news_alert', 'N')}"
795
+ volume_alert = f"{'🟢' if result.get('volume_alert') == 'Y' else '🔴'} {result.get('volume_alert', 'N')}"
796
+ institutional_alert = f"{'🟢' if result.get('institutional_alert') == 'Y' else '🔴'} {result.get('institutional_alert', 'N')}"
797
+ technical_alert = f"{'🟢' if result.get('technical_alert') == 'Y' else '🔴'} {result.get('technical_alert', 'N')}"
798
+
799
+ status = "✅ 成功" if not result['error_message'] else f"❌ {result['error_message'][:20]}..."
800
+
801
+ table_html += f"""
802
+ <tr style="background-color: {row_color};">
803
+ <td style="border: 1px solid #ddd; padding: 8px; font-weight: bold;">{result['symbol']}</td>
804
+ <td style="border: 1px solid #ddd; padding: 8px;">{result['name']}</td>
805
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['current_price']}</td>
806
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['up_probability']}</td>
807
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['down_probability']}</td>
808
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['sideways_probability']}</td>
809
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['confidence']}</td>
810
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: center; font-size: 12px;" title="{result.get('news_message', '')}">{news_alert}</td>
811
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: center; font-size: 12px;" title="{result.get('volume_message', '')}">{volume_alert}</td>
812
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: center; font-size: 12px;" title="{result.get('institutional_message', '')}">{institutional_alert}</td>
813
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: center; font-size: 12px;" title="{result.get('technical_message', '')}">{technical_alert}</td>
814
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: center;">{direction}</td>
815
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: center; font-size: 12px;">{holding_advice}</td>
816
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: center; font-size: 12px;">{buying_advice}</td>
817
+ <td style="border: 1px solid #ddd; padding: 8px;">{status}</td>
818
+ </tr>
819
+ """
820
+
821
+ table_html += """
822
+ </tbody>
823
+ </table>
824
+ </div>
825
+ <div style="margin-top: 10px; font-size: 12px; color: #666;">
826
+ <strong>🚨 Alert 說明:</strong><br>
827
+ 📰 <strong>新聞Alert:</strong> 公司重大公告(併購、BOT、合約、目標價上修)<br>
828
+ 📊 <strong>量能Alert:</strong> 當日量 >= 2×20日平均量且突破價格阻力<br>
829
+ 💰 <strong>籌碼Alert:</strong> 三大法人連續買超或外資當日淨買超↑<br>
830
+ 📈 <strong>技術Alert:</strong> 價格突破20日高且MACD交叉/OBV上升<br>
831
+ <strong>🟢 Y = 達到條件 | 🔴 N = 未達到條件</strong><br><br>
832
+
833
+ <strong>買賣建議說明:</strong><br>
834
+ 🔥強烈持有/🚀積極買進: 高機率上漲且高信心度<br>
835
+ ✅建議持有/💰適度買進: 中高機率上漲<br>
836
+ 🚨建議賣出/⛔不建議買進: 高機率下跌且高信心度<br>
837
+ ⚠️謹慎持有/🔍暫緩買進: 中高機率下跌<br>
838
+ 🤔觀望持有/⏳保持觀望: 低信心度預測<br>
839
+ 📊區間持有/🎯等待機會: 盤整機率較高<br>
840
+ <em>註:此建議僅供參考,請結合個人情況謹慎投資 - 由 Copilot 生成</em>
841
+ </div>
842
+ """
843
+
844
+ return table_html
845
+
846
+ def create_batch_analysis_charts(results):
847
+ """創建批次分析結果圖表"""
848
+ if not results:
849
+ return None, None, None, None
850
+
851
+ # 過濾出成功分析的結果
852
+ success_results = [r for r in results if r['error_message'] == '']
853
+
854
+ if not success_results:
855
+ return None, None, None, None
856
+
857
+ # 準備數據
858
+ symbols = [r['symbol'] for r in success_results]
859
+ up_probs = [float(r['up_probability']) for r in success_results]
860
+ down_probs = [float(r['down_probability']) for r in success_results]
861
+ sideways_probs = [float(r['sideways_probability']) for r in success_results]
862
+ confidence = [float(r['confidence']) for r in success_results]
863
+
864
+ # 1. 機率比較柱狀圖
865
+ fig_bar = go.Figure()
866
+ fig_bar.add_trace(go.Bar(name='上漲機率', x=symbols, y=up_probs, marker_color='green', opacity=0.8))
867
+ fig_bar.add_trace(go.Bar(name='下跌機率', x=symbols, y=down_probs, marker_color='red', opacity=0.8))
868
+ fig_bar.add_trace(go.Bar(name='盤整機率', x=symbols, y=sideways_probs, marker_color='gray', opacity=0.8))
869
+
870
+ fig_bar.update_layout(
871
+ title='📊 股票預測機率比較',
872
+ xaxis_title='股票代號',
873
+ yaxis_title='機率 (%)',
874
+ barmode='group',
875
+ height=500,
876
+ showlegend=True,
877
+ xaxis_tickangle=-45
878
+ )
879
+
880
+ # 2. 信心度散佈圖
881
+ fig_scatter = go.Figure()
882
+
883
+ # 根據最高機率決定顏色
884
+ colors = []
885
+ for i in range(len(success_results)):
886
+ if up_probs[i] > down_probs[i] and up_probs[i] > sideways_probs[i]:
887
+ colors.append('green') # 看多
888
+ elif down_probs[i] > up_probs[i] and down_probs[i] > sideways_probs[i]:
889
+ colors.append('red') # 看空
890
+ else:
891
+ colors.append('gray') # 盤整
892
+
893
+ fig_scatter.add_trace(go.Scatter(
894
+ x=symbols,
895
+ y=confidence,
896
+ mode='markers+text',
897
+ marker=dict(
898
+ size=[max(prob) for prob in zip(up_probs, down_probs, sideways_probs)],
899
+ sizemode='diameter',
900
+ sizeref=2,
901
+ color=colors,
902
+ opacity=0.7,
903
+ line=dict(width=2, color='white')
904
+ ),
905
+ text=[f"{conf:.1f}%" for conf in confidence],
906
+ textposition="middle center",
907
+ name='信心度'
908
+ ))
909
+
910
+ fig_scatter.update_layout(
911
+ title='🎯 預測信心度分佈 (圓圈大小=最高機率)',
912
+ xaxis_title='股票代號',
913
+ yaxis_title='信心度 (%)',
914
+ height=500,
915
+ xaxis_tickangle=-45
916
+ )
917
+
918
+ # 3. 綜合評分雷達圖 (取前6支股票)
919
+ radar_data = success_results[:6] # 限制顯示數量避免過於擁擠
920
+ fig_radar = go.Figure()
921
+
922
+ categories = ['上漲機率', '信心度', '綜合評分']
923
+
924
+ for i, result in enumerate(radar_data):
925
+ # 計算綜合評分 (上漲機率 * 信心度 / 100)
926
+ composite_score = float(result['up_probability']) * float(result['confidence']) / 100
927
+
928
+ values = [
929
+ float(result['up_probability']),
930
+ float(result['confidence']),
931
+ composite_score
932
+ ]
933
+
934
+ fig_radar.add_trace(go.Scatterpolar(
935
+ r=values + [values[0]], # 閉合雷達圖
936
+ theta=categories + [categories[0]],
937
+ fill='toself',
938
+ name=result['symbol'],
939
+ opacity=0.6
940
+ ))
941
+
942
+ fig_radar.update_layout(
943
+ polar=dict(
944
+ radialaxis=dict(
945
+ visible=True,
946
+ range=[0, 100]
947
+ )
948
+ ),
949
+ title='📈 股票綜合評分雷達圖 (前6支)',
950
+ height=500,
951
+ showlegend=True
952
+ )
953
+
954
+ # 4. 機率分佈餅圖統計
955
+ # 統計各種預測傾向的數量
956
+ bullish_count = sum(1 for r in success_results if float(r['up_probability']) > max(float(r['down_probability']), float(r['sideways_probability'])))
957
+ bearish_count = sum(1 for r in success_results if float(r['down_probability']) > max(float(r['up_probability']), float(r['sideways_probability'])))
958
+ neutral_count = len(success_results) - bullish_count - bearish_count
959
+
960
+ fig_pie = go.Figure(data=[go.Pie(
961
+ labels=['看多股票', '看空股票', '盤整股票'],
962
+ values=[bullish_count, bearish_count, neutral_count],
963
+ marker_colors=['green', 'red', 'gray'],
964
+ textinfo='label+percent+value',
965
+ hovertemplate='<b>%{label}</b><br>數量: %{value}<br>比例: %{percent}<extra></extra>'
966
+ )])
967
+
968
+ fig_pie.update_layout(
969
+ title='🥧 整體市場情緒分佈',
970
+ height=400
971
+ )
972
+
973
+ return fig_bar, fig_scatter, fig_radar, fig_pie
974
+
975
+ def batch_analyze_stocks(stock_list_input):
976
+ """批次分析股票清單"""
977
+ # 檢查輸入是否為空
978
+ if not stock_list_input or not stock_list_input.strip():
979
+ return "❌ 請輸入股票代碼!可用逗號、空格或換行分隔多個股票代碼。", "", None, None, None, None, ""
980
+
981
+ try:
982
+ # 解析股票清單(支援多種分隔符)
983
+ import re
984
+ stock_symbols = re.split(r'[,\s\n]+', stock_list_input.strip())
985
+ stock_symbols = [symbol.strip().upper() for symbol in stock_symbols if symbol.strip()]
986
+
987
+ if not stock_symbols:
988
+ return "❌ 未找到有效的股票代碼!", "", None, None, None, None, ""
989
+
990
+ # 準備結果列表
991
+ results = []
992
+ progress_messages = []
993
+
994
+ progress_messages.append(f"📊 開始批次分析 {len(stock_symbols)} 支股票...")
995
+
996
+ # 分析每支股票
997
+ for i, symbol in enumerate(stock_symbols, 1):
998
+ progress_messages.append(f"\n🔍 正在分析 ({i}/{len(stock_symbols)}): {symbol}")
999
+
1000
+ try:
1001
+ # 獲取股票數據
1002
+ result = analyzer.fetch_stock_data(symbol.upper())
1003
+ if len(result) == 3:
1004
+ success, message, stock_name = result
1005
+ else:
1006
+ success, message = result
1007
+ stock_name = symbol
1008
+
1009
+ if not success:
1010
+ # 記錄錯誤
1011
+ results.append({
1012
+ 'symbol': symbol,
1013
+ 'name': stock_name or symbol,
1014
+ 'current_price': 'N/A',
1015
+ 'up_probability': 'ERROR',
1016
+ 'down_probability': 'ERROR',
1017
+ 'sideways_probability': 'ERROR',
1018
+ 'confidence': 'ERROR',
1019
+ 'news_alert': 'N/A',
1020
+ 'volume_alert': 'N/A',
1021
+ 'institutional_alert': 'N/A',
1022
+ 'technical_alert': 'N/A',
1023
+ 'news_message': '',
1024
+ 'volume_message': '',
1025
+ 'institutional_message': '',
1026
+ 'technical_message': '',
1027
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
1028
+ 'error_message': message
1029
+ })
1030
+ progress_messages.append(f"❌ {symbol}: {message}")
1031
+ continue
1032
+
1033
+ # 計算技術指標
1034
+ df = analyzer.calculate_technical_indicators()
1035
+ if df is None or len(df) < 30:
1036
+ results.append({
1037
+ 'symbol': symbol,
1038
+ 'name': stock_name or symbol,
1039
+ 'current_price': 'N/A',
1040
+ 'up_probability': 'ERROR',
1041
+ 'down_probability': 'ERROR',
1042
+ 'sideways_probability': 'ERROR',
1043
+ 'confidence': 'ERROR',
1044
+ 'news_alert': 'N/A',
1045
+ 'volume_alert': 'N/A',
1046
+ 'institutional_alert': 'N/A',
1047
+ 'technical_alert': 'N/A',
1048
+ 'news_message': '',
1049
+ 'volume_message': '',
1050
+ 'institutional_message': '',
1051
+ 'technical_message': '',
1052
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
1053
+ 'error_message': '數據不足,無法分析'
1054
+ })
1055
+ progress_messages.append(f"❌ {symbol}: 數據不足")
1056
+ continue
1057
+
1058
+ # 獲取新聞情感
1059
+ news_sentiment = analyzer.get_news_sentiment(symbol)
1060
+ sentiment_summary = analyzer.analyze_sentiment_summary(news_sentiment)
1061
+
1062
+ # 計算預測機率
1063
+ recent_data = df.tail(20)
1064
+ technical_signals = []
1065
+
1066
+ # 簡化的技術信號計算
1067
+ latest = df.iloc[-1]
1068
+ if latest['Close'] > latest['MA20']:
1069
+ technical_signals.append("價格在20日均線之上")
1070
+ else:
1071
+ technical_signals.append("價格在20日均線之下")
1072
+
1073
+ probabilities = analyzer.calculate_prediction_probabilities(
1074
+ technical_signals, sentiment_summary, recent_data
1075
+ )
1076
+
1077
+ # 獲取股票資訊
1078
+ stock_info = analyzer.get_stock_info(symbol)
1079
+
1080
+ # 檢查四項 alert
1081
+ alerts = analyzer.check_alerts(recent_data, symbol)
1082
+
1083
+ # 記錄成功結果
1084
+ results.append({
1085
+ 'symbol': symbol,
1086
+ 'name': stock_info['name'],
1087
+ 'current_price': f"{latest['Close']:.2f}" if latest['Close'] else 'N/A',
1088
+ 'up_probability': f"{probabilities['up']:.1f}",
1089
+ 'down_probability': f"{probabilities['down']:.1f}",
1090
+ 'sideways_probability': f"{probabilities['sideways']:.1f}",
1091
+ 'confidence': f"{probabilities['confidence']*100:.1f}",
1092
+ 'news_alert': alerts['news_alert']['status'],
1093
+ 'volume_alert': alerts['volume_alert']['status'],
1094
+ 'institutional_alert': alerts['institutional_alert']['status'],
1095
+ 'technical_alert': alerts['technical_alert']['status'],
1096
+ 'news_message': alerts['news_alert']['message'],
1097
+ 'volume_message': alerts['volume_alert']['message'],
1098
+ 'institutional_message': alerts['institutional_alert']['message'],
1099
+ 'technical_message': alerts['technical_alert']['message'],
1100
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
1101
+ 'error_message': ''
1102
+ })
1103
+
1104
+ progress_messages.append(f"✅ {symbol}: 分析完成")
1105
+
1106
+ except Exception as e:
1107
+ # 處理未預期的錯誤
1108
+ results.append({
1109
+ 'symbol': symbol,
1110
+ 'name': symbol,
1111
+ 'current_price': 'N/A',
1112
+ 'up_probability': 'ERROR',
1113
+ 'down_probability': 'ERROR',
1114
+ 'sideways_probability': 'ERROR',
1115
+ 'confidence': 'ERROR',
1116
+ 'news_alert': 'N/A',
1117
+ 'volume_alert': 'N/A',
1118
+ 'institutional_alert': 'N/A',
1119
+ 'technical_alert': 'N/A',
1120
+ 'news_message': '',
1121
+ 'volume_message': '',
1122
+ 'institutional_message': '',
1123
+ 'technical_message': '',
1124
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
1125
+ 'error_message': f'未預期錯誤: {str(e)}'
1126
+ })
1127
+ progress_messages.append(f"❌ {symbol}: 未預期錯誤")
1128
+
1129
+ # 統計結果
1130
+ success_count = len([r for r in results if r['error_message'] == ''])
1131
+ error_count = len(results) - success_count
1132
+
1133
+ summary_message = f"""
1134
+ 📈 批次分析完成!
1135
+ 📊 **分析統計:**
1136
+ - 總計股票數:{len(stock_symbols)}
1137
+ - 成功分析:{success_count}
1138
+ - 分析失敗:{error_count}
1139
+
1140
+ 📊 **圖表已生成:**
1141
+ - 📊 機率比較柱狀圖
1142
+ - 🎯 信心度散佈圖
1143
+ - 📈 綜合評分雷達圖
1144
+ - 🥧 市場情緒餅圖
1145
+
1146
+ 🎯 **請查看下方圖表進行投資決策分析!**
1147
+ """
1148
+
1149
+ progress_log = "\n".join(progress_messages)
1150
+
1151
+ # 創建圖表和結果表格
1152
+ chart_bar, chart_scatter, chart_radar, chart_pie = create_batch_analysis_charts(results)
1153
+ results_table = create_results_table(results)
1154
+
1155
+ return summary_message, progress_log, chart_bar, chart_scatter, chart_radar, chart_pie, results_table
1156
+
1157
+ except Exception as e:
1158
+ return f"❌ 批次分析過程中發生錯誤:{str(e)}", "", None, None, None, None, ""
1159
+
1160
+ # 創建 Gradio 界面
1161
+ with gr.Blocks(title="AI 股票分析師", theme=gr.themes.Soft()) as app:
1162
+ gr.Markdown(
1163
+ """
1164
+ # 📈 AI 股票分析師
1165
+
1166
+ ### 🤖 使用 Hugging Face 模型進行智能股票分析
1167
+
1168
+ **✨ 核心功能:**
1169
+ - 📊 **完整技術指標**:MA、RSI、MACD、布林通道分析
1170
+ - 🧠 **AI 情感分析**:使用 FinBERT 模型分析市場情緒
1171
+ - 🎯 **機率預測**:提供上漲/下跌/盤整機率百分比
1172
+ - 🚨 **四項 Alert 提醒**:新聞/公告、量能、籌碼、技術面警示
1173
+ - 💼 **買賣建議**:針對持股/未持股提供具體投資建議
1174
+ - 📈 **智能策略**:根據信心度給出個性化投資策略
1175
+ - 🎚️ **倉位建議**:提供合理的倉位配置建議
1176
+ - 🖼️ **互動圖表**:動態視覺化技術指標走勢
1177
+ - 📁 **批次分析**:一次分析多支股票並匯出詳細報告
1178
+
1179
+ **🚀 使用方法:** 單支分析輸入股票代碼,批次分析直接在文本框中輸入多個股票代碼即可!
1180
+
1181
+ **💡 新功能亮點:** 現在提供差異化的買賣建議 - 持有股票時建議觀望/賣出/持有,未持有時建議加碼買進/觀望!
1182
+ """
1183
+ )
1184
+
1185
+ # 建立分頁
1186
+ with gr.Tabs():
1187
+ with gr.TabItem("🎯 單支股票分析"):
1188
+ with gr.Row():
1189
+ with gr.Column(scale=1):
1190
+ stock_input = gr.Textbox(
1191
+ label="股票代碼",
1192
+ placeholder="例如:AAPL, TSLA, 2330.TW",
1193
+ value="2330.TW"
1194
+ )
1195
+ analyze_btn = gr.Button("開始分析", variant="primary", size="lg")
1196
+
1197
+ status_output = gr.Textbox(
1198
+ label="分析狀態",
1199
+ lines=2,
1200
+ interactive=False
1201
+ )
1202
+
1203
+ with gr.Column(scale=2):
1204
+ chart_output = gr.Plot(label="股價走勢圖")
1205
+
1206
+ prediction_output = gr.Markdown(label="AI 分析報告")
1207
+
1208
+ # 事件綁定
1209
+ analyze_btn.click(
1210
+ fn=analyze_stock,
1211
+ inputs=[stock_input],
1212
+ outputs=[chart_output, status_output, prediction_output]
1213
+ )
1214
+
1215
+ # 範例按鈕
1216
+ gr.Examples(
1217
+ examples=[
1218
+ ["AAPL"],
1219
+ ["TSLA"],
1220
+ ["2330.TW"],
1221
+ ["MSFT"],
1222
+ ["GOOGL"]
1223
+ ],
1224
+ inputs=[stock_input]
1225
+ )
1226
+
1227
+ with gr.TabItem("📊 批次股票分析"):
1228
+ gr.Markdown(
1229
+ """
1230
+ ### 📁 批次分析功能
1231
+
1232
+ **📋 使用步驟:**
1233
+ 1. 在下方文本框中輸入股票代碼
1234
+ 2. 支援多種分隔方式:逗號、空格或換行
1235
+ 3. 例如:`AAPL, TSLA, 2330.TW` 或 `AAPL MSFT GOOGL`
1236
+ 4. 點擊「開始批次分析」按鈕查看結果
1237
+
1238
+ **📈 輸出內容:**
1239
+ - 📊 機率比較柱狀圖:直觀對比各股票預測機率
1240
+ - 🎯 信心度散佈圖:顯示預測可靠性分佈
1241
+ - 📈 綜合評分雷達圖:多維度股票評分比較
1242
+ - 🥧 市場情緒餅圖:整體多空情緒統計
1243
+ - � **四項 Alert 提醒**:新聞、量能、籌碼、技術面警示
1244
+ - �💼 **買賣建議表格**:針對持股與非持股提供具體建議
1245
+ - 📊 **詳細分析表格**:包含所有機率、信心度與投資建議
1246
+ - 即時進度顯示和完整錯誤處理
1247
+
1248
+ **💡 買賣建議特色:**
1249
+ - 🏠 **持股建議**:持有、賣出、觀望等具體行動
1250
+ - 💰 **買進建議**:積極買進、適度買進、暫緩等建議
1251
+ - 🎚️ **倉位建議**:根據信心度提供合理倉位配置
1252
+ - ⚠️ **風險警示**:超買超賣等特殊情況提醒
1253
+ """
1254
+ )
1255
+
1256
+ # 股票代碼輸入區域
1257
+ stock_list_input = gr.Textbox(
1258
+ label="📝 輸入股票代碼",
1259
+ placeholder="例如:AAPL, TSLA, 2330.TW, MSFT, GOOGL\n或用空格、換行分隔",
1260
+ lines=3,
1261
+ value="AAPL, TSLA, 2330.TW, MSFT, GOOGL",
1262
+ info="支援逗號、空格或換行分隔多個股票代碼"
1263
+ )
1264
+
1265
+ # 範例按鈕
1266
+ gr.Examples(
1267
+ examples=[
1268
+ ["AAPL, MSFT, GOOGL, AMZN, TSLA"],
1269
+ ["2330.TW, 2317.TW, 2454.TW, 3711.TW, 2382.TW"],
1270
+ ["JPM, BAC, WFC, C, GS"],
1271
+ ["JNJ, PFE, ABBV, UNH, CVS"],
1272
+ ["BTC-USD, ETH-USD, ^GSPC, ^DJI, ^IXIC"]
1273
+ ],
1274
+ inputs=[stock_list_input],
1275
+ label="💡 快速範例"
1276
+ )
1277
+
1278
+ with gr.Row():
1279
+ batch_analyze_btn = gr.Button(
1280
+ "🚀 開始批次分析",
1281
+ variant="primary",
1282
+ size="lg",
1283
+ scale=1
1284
+ )
1285
+
1286
+ with gr.Row():
1287
+ with gr.Column(scale=1):
1288
+ batch_summary = gr.Markdown(label="📊 分析摘要")
1289
+ with gr.Column(scale=1):
1290
+ batch_progress = gr.Textbox(
1291
+ label="📋 分析進度",
1292
+ lines=10,
1293
+ interactive=False,
1294
+ max_lines=15
1295
+ )
1296
+
1297
+ # 圖表顯示區域
1298
+ gr.Markdown("## 📈 視覺化分析結果")
1299
+
1300
+ with gr.Row():
1301
+ with gr.Column(scale=1):
1302
+ chart_probability = gr.Plot(label="📊 股票預測機率比較")
1303
+ with gr.Column(scale=1):
1304
+ chart_confidence = gr.Plot(label="🎯 預測信心度分佈")
1305
+
1306
+ with gr.Row():
1307
+ with gr.Column(scale=1):
1308
+ chart_radar = gr.Plot(label="📈 綜合評分雷達圖")
1309
+ with gr.Column(scale=1):
1310
+ chart_sentiment = gr.Plot(label="🥧 整體市場情緒分佈")
1311
+
1312
+ # 詳細結果表格
1313
+ gr.Markdown("## 📋 詳細分析結果")
1314
+ results_table = gr.HTML(label="分析結果表格")
1315
+
1316
+ # 批次分析事件綁定
1317
+ batch_analyze_btn.click(
1318
+ fn=batch_analyze_stocks,
1319
+ inputs=[stock_list_input],
1320
+ outputs=[
1321
+ batch_summary,
1322
+ batch_progress,
1323
+ chart_probability,
1324
+ chart_confidence,
1325
+ chart_radar,
1326
+ chart_sentiment,
1327
+ results_table
1328
+ ]
1329
+ )
1330
+
1331
+ # 啟動應用
1332
+ if __name__ == "__main__":
1333
+ print("正在啟動 AI 股票分析師...")
1334
+
1335
+ # 簡化的啟動邏輯
1336
+ try:
1337
+ if IS_HUGGINGFACE_SPACE:
1338
+ # Hugging Face Spaces 環境 - 使用預設配置
1339
+ print("在 Hugging Face Spaces 中啟動...")
1340
+ app.launch()
1341
+ else:
1342
+ # 本地環境 - 嘗試多個端口
1343
+ print("在本地環境中啟動...")
1344
+ ports_to_try = [7860, 7861, 7862, 7863, 7864, 7865]
1345
+
1346
+ launched = False
1347
+ for port in ports_to_try:
1348
+ try:
1349
+ print(f"嘗試端口 {port}...")
1350
+ app.launch(
1351
+ share=True,
1352
+ server_name="0.0.0.0",
1353
+ server_port=port,
1354
+ show_error=True,
1355
+ quiet=False
1356
+ )
1357
+ launched = True
1358
+ break
1359
+ except OSError as e:
1360
+ if "port" in str(e).lower():
1361
+ print(f"端口 {port} 不可用,嘗試下一個...")
1362
+ continue
1363
+ else:
1364
+ raise e
1365
+
1366
+ if not launched:
1367
+ print("所有預設端口都被佔用,使用隨機端口...")
1368
+ app.launch(
1369
+ share=True,
1370
+ server_name="0.0.0.0",
1371
+ server_port=0, # 0 表示自動分配端口
1372
+ show_error=True
1373
+ )
1374
+
1375
+ except Exception as e:
1376
+ print(f"啟動失敗: {e}")
1377
+ print("請檢查端口使用情況或嘗試重新啟動")
1378
+ raise e