54justin commited on
Commit
d7cd8fa
·
verified ·
1 Parent(s): 46d486c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +827 -0
app.py ADDED
@@ -0,0 +1,827 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 由 Copilot 生成 - AI 股票分析師
2
+ import subprocess
3
+ import sys
4
+ import os
5
+
6
+ # 環境檢測
7
+ IS_HUGGINGFACE_SPACE = "SPACE_ID" in os.environ
8
+ print(f"運行環境: {'Hugging Face Spaces' if IS_HUGGINGFACE_SPACE else '本地環境'}")
9
+
10
+ # 檢查並安裝所需套件的函數
11
+ def install_package(package_name):
12
+ try:
13
+ __import__(package_name)
14
+ except ImportError:
15
+ print(f"正在安裝 {package_name}...")
16
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
17
+
18
+ # 安裝必要套件
19
+ required_packages = [
20
+ "torch>=2.0.0",
21
+ "torchvision>=0.15.0",
22
+ "torchaudio>=2.0.0",
23
+ "yfinance>=0.2.18",
24
+ "gradio>=4.0.0",
25
+ "pandas>=1.5.0",
26
+ "numpy>=1.21.0",
27
+ "matplotlib>=3.5.0",
28
+ "plotly>=5.0.0",
29
+ "beautifulsoup4>=4.11.0",
30
+ "requests>=2.28.0",
31
+ "transformers>=4.21.0",
32
+ "accelerate>=0.20.0",
33
+ "tokenizers>=0.13.0"
34
+ ]
35
+
36
+ for package in required_packages:
37
+ package_name = package.split(">=")[0].split("==")[0]
38
+ if package_name == "beautifulsoup4":
39
+ package_name = "bs4"
40
+ try:
41
+ __import__(package_name)
42
+ except ImportError:
43
+ print(f"正在安裝 {package}...")
44
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package])
45
+
46
+ # 現在導入所有套件
47
+ import gradio as gr
48
+ import yfinance as yf
49
+ import pandas as pd
50
+ import numpy as np
51
+ import matplotlib.pyplot as plt
52
+ import plotly.graph_objects as go
53
+ import plotly.express as px
54
+ from datetime import datetime, timedelta
55
+ import requests
56
+ from bs4 import BeautifulSoup
57
+ from transformers import pipeline
58
+ import warnings
59
+ import csv
60
+ import os
61
+ from datetime import datetime
62
+ warnings.filterwarnings('ignore')
63
+
64
+ # 初始化 Hugging Face 模型
65
+ print("正在載入 AI 模型...")
66
+
67
+ # 嘗試載入模型,如果失敗則使用較輕量的替代方案
68
+ try:
69
+ sentiment_analyzer = pipeline("sentiment-analysis", model="ProsusAI/finbert")
70
+ print("FinBERT 情感分析模型載入成功")
71
+ except Exception as e:
72
+ print(f"FinBERT 載入失敗,嘗試替代模型: {e}")
73
+ try:
74
+ sentiment_analyzer = pipeline("sentiment-analysis", model="nlptown/bert-base-multilingual-uncased-sentiment")
75
+ print("多語言情感分析模型載入成功")
76
+ except Exception as e2:
77
+ print(f"替代模型載入失敗: {e2}")
78
+ sentiment_analyzer = None
79
+
80
+ try:
81
+ summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
82
+ print("BART 摘要模型載入成功")
83
+ except Exception as e:
84
+ print(f"BART 載入失敗,嘗試替代模型: {e}")
85
+ try:
86
+ summarizer = pipeline("summarization", model="sshleifer/distilbart-cnn-12-6")
87
+ print("DistilBART 摘要模型載入成功")
88
+ except Exception as e2:
89
+ print(f"摘要模型載入失敗: {e2}")
90
+ summarizer = None
91
+
92
+ class StockAnalyzer:
93
+ def __init__(self):
94
+ self.data = None
95
+ self.symbol = None
96
+
97
+ def fetch_stock_data(self, symbol, period="1y"):
98
+ """獲取股票歷史數據"""
99
+ try:
100
+ ticker = yf.Ticker(symbol)
101
+ self.data = ticker.history(period=period)
102
+ self.symbol = symbol
103
+ # 獲取股票資訊
104
+ info = ticker.info
105
+ stock_name = info.get('longName', info.get('shortName', symbol))
106
+ return True, f"成功獲取 {symbol} 的歷史數據", stock_name
107
+ except Exception as e:
108
+ return False, f"數據獲取失敗: {str(e)}", None
109
+
110
+ def get_stock_info(self, symbol):
111
+ """獲取股票基本資訊"""
112
+ try:
113
+ ticker = yf.Ticker(symbol)
114
+ info = ticker.info
115
+ current_price = self.data['Close'].iloc[-1] if self.data is not None else None
116
+ stock_name = info.get('longName', info.get('shortName', symbol))
117
+ return {
118
+ 'name': stock_name,
119
+ 'current_price': current_price,
120
+ 'symbol': symbol
121
+ }
122
+ except Exception as e:
123
+ return {
124
+ 'name': symbol,
125
+ 'current_price': None,
126
+ 'symbol': symbol
127
+ }
128
+
129
+ def calculate_technical_indicators(self):
130
+ """計算技術指標"""
131
+ if self.data is None:
132
+ return None
133
+
134
+ df = self.data.copy()
135
+
136
+ # 移動平均線
137
+ df['MA5'] = df['Close'].rolling(window=5).mean()
138
+ df['MA20'] = df['Close'].rolling(window=20).mean()
139
+ df['MA60'] = df['Close'].rolling(window=60).mean()
140
+
141
+ # RSI 相對強弱指標
142
+ delta = df['Close'].diff()
143
+ gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
144
+ loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
145
+ rs = gain / loss
146
+ df['RSI'] = 100 - (100 / (1 + rs))
147
+
148
+ # MACD
149
+ exp1 = df['Close'].ewm(span=12).mean()
150
+ exp2 = df['Close'].ewm(span=26).mean()
151
+ df['MACD'] = exp1 - exp2
152
+ df['MACD_signal'] = df['MACD'].ewm(span=9).mean()
153
+
154
+ # 布林通道
155
+ df['BB_middle'] = df['Close'].rolling(window=20).mean()
156
+ bb_std = df['Close'].rolling(window=20).std()
157
+ df['BB_upper'] = df['BB_middle'] + (bb_std * 2)
158
+ df['BB_lower'] = df['BB_middle'] - (bb_std * 2)
159
+
160
+ return df
161
+
162
+ def get_news_sentiment(self, symbol):
163
+ """獲取並分析新聞情感"""
164
+ # 這裡簡化處理,實際應用中需要更穩定的新聞 API
165
+ try:
166
+ # 模擬新聞標題(實際應用中需要接入新聞 API)
167
+ sample_news = [
168
+ f"{symbol} 股價創新高,投資人信心大增",
169
+ f"市場關注 {symbol} 最新財報表現",
170
+ f"{symbol} 面臨供應鏈挑戰,股價承壓",
171
+ f"分析師上調 {symbol} 目標價,看好後市",
172
+ f"{symbol} 技術創新獲得市場認可"
173
+ ]
174
+
175
+ sentiments = []
176
+
177
+ # 檢查情感分析模型是否可用
178
+ if sentiment_analyzer is None:
179
+ # 如果模型不可用,返回模擬的情感分析結果
180
+ for news in sample_news:
181
+ # 簡單的關鍵詞情感分析替代方案
182
+ positive_words = ['創新高', '信心大增', '上調', '看好', '創新', '獲得認可']
183
+ negative_words = ['挑戰', '承壓', '面臨', '下滑']
184
+
185
+ score = 0.5 # 中性
186
+ sentiment = 'NEUTRAL'
187
+
188
+ for word in positive_words:
189
+ if word in news:
190
+ score = 0.8
191
+ sentiment = 'POSITIVE'
192
+ break
193
+
194
+ for word in negative_words:
195
+ if word in news:
196
+ score = 0.8
197
+ sentiment = 'NEGATIVE'
198
+ break
199
+
200
+ sentiments.append({
201
+ 'text': news,
202
+ 'sentiment': sentiment,
203
+ 'score': score
204
+ })
205
+ else:
206
+ # 使用 AI 模型進行情感分析
207
+ for news in sample_news:
208
+ result = sentiment_analyzer(news)[0]
209
+ sentiments.append({
210
+ 'text': news,
211
+ 'sentiment': result['label'],
212
+ 'score': result['score']
213
+ })
214
+
215
+ return sentiments
216
+
217
+ except Exception as e:
218
+ return [{'text': f'新聞分析暫時無法使用: {str(e)}', 'sentiment': 'NEUTRAL', 'score': 0.5}]
219
+
220
+ def generate_prediction(self, df, news_sentiment):
221
+ """生成預測分析"""
222
+ if df is None or len(df) < 30:
223
+ return "數據不足,無法進行預測分析"
224
+
225
+ # 獲取最新數據
226
+ latest = df.iloc[-1]
227
+ recent_data = df.tail(20)
228
+
229
+ # 技術分析信號
230
+ technical_signals = []
231
+
232
+ # 價格趨勢
233
+ if latest['Close'] > latest['MA20']:
234
+ technical_signals.append("價格在20日均線之上(多頭信號)")
235
+ else:
236
+ technical_signals.append("價格在20日均線之下(空頭信號)")
237
+
238
+ # RSI 分析
239
+ rsi = latest['RSI']
240
+ if rsi > 70:
241
+ technical_signals.append(f"?RSI({rsi:.1f}) 超買警訊")
242
+ elif rsi < 30:
243
+ technical_signals.append(f"RSI({rsi:.1f}) 超賣機會")
244
+ else:
245
+ technical_signals.append(f"RSI({rsi:.1f}) 正常範圍")
246
+
247
+ # MACD 分析
248
+ if latest['MACD'] > latest['MACD_signal']:
249
+ technical_signals.append("MACD 呈現多頭排列")
250
+ else:
251
+ technical_signals.append("MACD 呈現空頭排列")
252
+
253
+ # 新聞情感分析
254
+ sentiment_summary = self.analyze_sentiment_summary(news_sentiment)
255
+
256
+ # 綜合預測
257
+ prediction = self.generate_comprehensive_prediction(technical_signals, sentiment_summary, recent_data)
258
+
259
+ return prediction
260
+
261
+ def analyze_sentiment_summary(self, sentiments):
262
+ """分析情感摘要"""
263
+ if not sentiments:
264
+ return "中性"
265
+
266
+ positive_count = sum(1 for s in sentiments if s['sentiment'] == 'POSITIVE')
267
+ negative_count = sum(1 for s in sentiments if s['sentiment'] == 'NEGATIVE')
268
+
269
+ if positive_count > negative_count:
270
+ return "偏樂觀"
271
+ elif negative_count > positive_count:
272
+ return "偏悲觀"
273
+ else:
274
+ return "中性"
275
+
276
+ def calculate_prediction_probabilities(self, technical_signals, sentiment, recent_data):
277
+ """計算上漲和下跌機率"""
278
+ # 計算技術面得分
279
+ bullish_signals = sum(1 for signal in technical_signals if "多頭" in signal or "機會" in signal)
280
+ bearish_signals = sum(1 for signal in technical_signals if "空頭" in signal or "警訊" in signal)
281
+ neutral_signals = len(technical_signals) - bullish_signals - bearish_signals
282
+
283
+ # 技術面得分 (-1 到 1)
284
+ total_signals = len(technical_signals)
285
+ if total_signals > 0:
286
+ tech_score = (bullish_signals - bearish_signals) / total_signals
287
+ else:
288
+ tech_score = 0
289
+
290
+ # 情感得分 (-1 到 1)
291
+ sentiment_score = 0
292
+ if sentiment == "偏樂觀":
293
+ sentiment_score = 0.6
294
+ elif sentiment == "偏悲觀":
295
+ sentiment_score = -0.6
296
+ else:
297
+ sentiment_score = 0
298
+
299
+ # 價格動量得分
300
+ price_change = ((recent_data['Close'].iloc[-1] - recent_data['Close'].iloc[-5]) / recent_data['Close'].iloc[-5]) * 100
301
+ momentum_score = np.tanh(price_change / 10) # 標準化到 -1 到 1
302
+
303
+ # RSI 得分
304
+ latest = recent_data.iloc[-1]
305
+ rsi = latest.get('RSI', 50)
306
+ if rsi > 70:
307
+ rsi_score = -0.5 # 超買,偏空
308
+ elif rsi < 30:
309
+ rsi_score = 0.5 # 超賣,偏多
310
+ else:
311
+ rsi_score = (50 - rsi) / 100 # 標準化
312
+
313
+ # MACD 得分
314
+ macd_score = 0
315
+ if 'MACD' in latest and 'MACD_signal' in latest:
316
+ if latest['MACD'] > latest['MACD_signal']:
317
+ macd_score = 0.3
318
+ else:
319
+ macd_score = -0.3
320
+
321
+ # 綜合得分計算(加權平均)
322
+ weights = {
323
+ 'tech': 0.25,
324
+ 'sentiment': 0.20,
325
+ 'momentum': 0.25,
326
+ 'rsi': 0.15,
327
+ 'macd': 0.15
328
+ }
329
+
330
+ total_score = (
331
+ tech_score * weights['tech'] +
332
+ sentiment_score * weights['sentiment'] +
333
+ momentum_score * weights['momentum'] +
334
+ rsi_score * weights['rsi'] +
335
+ macd_score * weights['macd']
336
+ )
337
+
338
+ # 將得分轉換為機率 (使用 sigmoid 函數)
339
+ def sigmoid(x):
340
+ return 1 / (1 + np.exp(-x * 3)) # 放大 3 倍讓機率更明顯
341
+
342
+ up_probability = sigmoid(total_score) * 100
343
+ down_probability = sigmoid(-total_score) * 100
344
+ sideways_probability = 100 - up_probability - down_probability
345
+
346
+ # 確保機率總和為 100%
347
+ total_prob = up_probability + down_probability + sideways_probability
348
+ up_probability = (up_probability / total_prob) * 100
349
+ down_probability = (down_probability / total_prob) * 100
350
+ sideways_probability = (sideways_probability / total_prob) * 100
351
+
352
+ return {
353
+ 'up': max(15, min(75, up_probability)), # 限制在 15%-75% 範圍內
354
+ 'down': max(15, min(75, down_probability)), # 限制在 15%-75% 範圍內
355
+ 'sideways': max(10, sideways_probability), # 至少 10%
356
+ 'confidence': abs(total_score) # 信心度
357
+ }
358
+
359
+ def generate_comprehensive_prediction(self, technical_signals, sentiment, recent_data):
360
+ """生成綜合預測報告"""
361
+ # 計算價格變化
362
+ price_change = ((recent_data['Close'].iloc[-1] - recent_data['Close'].iloc[-5]) / recent_data['Close'].iloc[-5]) * 100
363
+
364
+ # 計算預測機率
365
+ probabilities = self.calculate_prediction_probabilities(technical_signals, sentiment, recent_data)
366
+
367
+ # 確定主要預測方向
368
+ max_prob = max(probabilities['up'], probabilities['down'], probabilities['sideways'])
369
+ if probabilities['up'] == max_prob:
370
+ main_direction = "看多"
371
+ direction_emoji = "📈"
372
+ elif probabilities['down'] == max_prob:
373
+ main_direction = "看空"
374
+ direction_emoji = "📉"
375
+ else:
376
+ main_direction = "盤整"
377
+ direction_emoji = "➡️"
378
+
379
+ # 信心度描述
380
+ confidence = probabilities['confidence']
381
+ if confidence > 0.4:
382
+ confidence_desc = "高信心"
383
+ elif confidence > 0.2:
384
+ confidence_desc = "中等信心"
385
+ else:
386
+ confidence_desc = "低信心"
387
+
388
+ report = f"""
389
+ ## 📊 {self.symbol} AI 分析報告
390
+
391
+ ### 📈 技術面分析:
392
+ {chr(10).join(f"• {signal}" for signal in technical_signals)}
393
+
394
+ ### 💭 市場情感:{sentiment}
395
+
396
+ ### 📊 近期表現:
397
+ - 5日漲跌幅:{price_change:+.2f}%
398
+ - 當前價位:${recent_data['Close'].iloc[-1]:.2f}
399
+
400
+ ### 🤖 AI 預測機率(短期 1-7天):
401
+
402
+ | 方向 | 機率 | 說明 |
403
+ |------|------|------|
404
+ | 📈 **上漲** | **{probabilities['up']:.1f}%** | 股價向上突破的可能性 |
405
+ | 📉 **下跌** | **{probabilities['down']:.1f}%** | 股價向下修正的可能性 |
406
+ | ➡️ **盤整** | **{probabilities['sideways']:.1f}%** | 股價維持震盪的可���性 |
407
+
408
+ ### 🎯 主要預測方向:
409
+ {direction_emoji} **{main_direction}** ({confidence_desc} - {confidence*100:.0f}%)
410
+
411
+ ### 📋 投資建議:
412
+ """
413
+
414
+ # 根據最高機率給出建議
415
+ if probabilities['up'] > 50:
416
+ report += """
417
+ - 💡 **多頭策略**:考慮逢低加碼或持有現有部位
418
+ - 🎯 **目標設定**:關注上方阻力位,設定合理獲利目標
419
+ - 🛡️ **風險管理**:設置止損點保護資本"""
420
+ elif probabilities['down'] > 50:
421
+ report += """
422
+ - 💡 **防守策略**:考慮減碼或等待更佳進場點
423
+ - 🎯 **支撐觀察**:留意下方支撐位是否守住
424
+ - 🛡️ **風險管理**:避免追高,控制倉位大小"""
425
+ else:
426
+ report += """
427
+ - 💡 **中性策略**:保持觀望,等待明確方向訊號
428
+ - 🎯 **區間操作**:可考慮在支撐阻力區間內操作
429
+ - 🛡️ **風險管理**:小部位測試,嚴格執行停損"""
430
+
431
+ report += f"""
432
+
433
+ ### 📅 中期展望(1個月):
434
+ 基於當前技術面和市場情緒分析,建議持續關注:
435
+ - 關鍵技術位:支撐與阻力區間
436
+ - 市場情緒變化:新聞面和資金流向
437
+ - 整體大盤走勢:系統性風險評估
438
+
439
+ ⚠️ **風險提醒**:此分析基於歷史數據和 AI 模型預測,僅供參考。投資有風險,請謹慎評估並做好風險管理!
440
+
441
+ ---
442
+ *預測信心度:{confidence*100:.0f}% | 分析時間:{datetime.now().strftime('%Y-%m-%d %H:%M')}*
443
+ """
444
+
445
+ return report
446
+
447
+ # 創建分析器實例
448
+ analyzer = StockAnalyzer()
449
+
450
+ def analyze_stock(symbol):
451
+ """主要分析函數"""
452
+ if not symbol.strip():
453
+ return None, "請輸入股票代碼", ""
454
+
455
+ # 獲取數據
456
+ result = analyzer.fetch_stock_data(symbol.upper())
457
+ if len(result) == 3:
458
+ success, message, stock_name = result
459
+ else:
460
+ success, message = result
461
+ stock_name = None
462
+
463
+ if not success:
464
+ return None, message, ""
465
+
466
+ # 計算技術指標
467
+ df = analyzer.calculate_technical_indicators()
468
+
469
+ # 創建價格圖表
470
+ fig = go.Figure()
471
+
472
+ # 添加K線圖
473
+ fig.add_trace(go.Candlestick(
474
+ x=df.index,
475
+ open=df['Open'],
476
+ high=df['High'],
477
+ low=df['Low'],
478
+ close=df['Close'],
479
+ name='價格'
480
+ ))
481
+
482
+ # 添加移動平均線
483
+ fig.add_trace(go.Scatter(x=df.index, y=df['MA5'], name='MA5', line=dict(color='orange')))
484
+ fig.add_trace(go.Scatter(x=df.index, y=df['MA20'], name='MA20', line=dict(color='blue')))
485
+
486
+ fig.update_layout(
487
+ title=f'{symbol} 股價走勢與技術指標',
488
+ xaxis_title='日期',
489
+ yaxis_title='價格',
490
+ height=600
491
+ )
492
+
493
+ # 獲取新聞情感
494
+ news_sentiment = analyzer.get_news_sentiment(symbol)
495
+
496
+ # 生成預測
497
+ prediction = analyzer.generate_prediction(df, news_sentiment)
498
+
499
+ return fig, "分析完成!", prediction
500
+
501
+ def batch_analyze_stocks():
502
+ """批次分析股票清單"""
503
+ stock_list_file = "StockList.txt"
504
+ result_file = "StockResult.csv"
505
+
506
+ # 檢查股票清單檔案是否存在
507
+ if not os.path.exists(stock_list_file):
508
+ return f"❌ 找不到 {stock_list_file} 檔案!請確認檔案存在。", ""
509
+
510
+ try:
511
+ # 讀取股票清單
512
+ with open(stock_list_file, 'r', encoding='utf-8') as f:
513
+ stock_symbols = [line.strip() for line in f if line.strip()]
514
+
515
+ if not stock_symbols:
516
+ return "❌ 股票清單檔案為空!", ""
517
+
518
+ # 準備結果列表
519
+ results = []
520
+ progress_messages = []
521
+
522
+ progress_messages.append(f"📊 開始批次分析 {len(stock_symbols)} 支股票...")
523
+
524
+ # 分析每支股票
525
+ for i, symbol in enumerate(stock_symbols, 1):
526
+ progress_messages.append(f"\n🔍 正在分析 ({i}/{len(stock_symbols)}): {symbol}")
527
+
528
+ try:
529
+ # 獲取股票數據
530
+ result = analyzer.fetch_stock_data(symbol.upper())
531
+ if len(result) == 3:
532
+ success, message, stock_name = result
533
+ else:
534
+ success, message = result
535
+ stock_name = symbol
536
+
537
+ if not success:
538
+ # 記錄錯誤
539
+ results.append({
540
+ 'symbol': symbol,
541
+ 'name': stock_name or symbol,
542
+ 'current_price': 'N/A',
543
+ 'up_probability': 'ERROR',
544
+ 'down_probability': 'ERROR',
545
+ 'sideways_probability': 'ERROR',
546
+ 'confidence': 'ERROR',
547
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
548
+ 'error_message': message
549
+ })
550
+ progress_messages.append(f"❌ {symbol}: {message}")
551
+ continue
552
+
553
+ # 計算技術指標
554
+ df = analyzer.calculate_technical_indicators()
555
+ if df is None or len(df) < 30:
556
+ results.append({
557
+ 'symbol': symbol,
558
+ 'name': stock_name or symbol,
559
+ 'current_price': 'N/A',
560
+ 'up_probability': 'ERROR',
561
+ 'down_probability': 'ERROR',
562
+ 'sideways_probability': 'ERROR',
563
+ 'confidence': 'ERROR',
564
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
565
+ 'error_message': '數據不足,無法分析'
566
+ })
567
+ progress_messages.append(f"❌ {symbol}: 數據不足")
568
+ continue
569
+
570
+ # 獲取新聞情感
571
+ news_sentiment = analyzer.get_news_sentiment(symbol)
572
+ sentiment_summary = analyzer.analyze_sentiment_summary(news_sentiment)
573
+
574
+ # 計算預測機率
575
+ recent_data = df.tail(20)
576
+ technical_signals = []
577
+
578
+ # 簡化的技術信號計算
579
+ latest = df.iloc[-1]
580
+ if latest['Close'] > latest['MA20']:
581
+ technical_signals.append("價格在20日均線之上")
582
+ else:
583
+ technical_signals.append("價格在20日均線之下")
584
+
585
+ probabilities = analyzer.calculate_prediction_probabilities(
586
+ technical_signals, sentiment_summary, recent_data
587
+ )
588
+
589
+ # 獲取股票資訊
590
+ stock_info = analyzer.get_stock_info(symbol)
591
+
592
+ # 記錄成功結果
593
+ results.append({
594
+ 'symbol': symbol,
595
+ 'name': stock_info['name'],
596
+ 'current_price': f"{latest['Close']:.2f}" if latest['Close'] else 'N/A',
597
+ 'up_probability': f"{probabilities['up']:.1f}",
598
+ 'down_probability': f"{probabilities['down']:.1f}",
599
+ 'sideways_probability': f"{probabilities['sideways']:.1f}",
600
+ 'confidence': f"{probabilities['confidence']*100:.1f}",
601
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
602
+ 'error_message': ''
603
+ })
604
+
605
+ progress_messages.append(f"✅ {symbol}: 分析完成")
606
+
607
+ except Exception as e:
608
+ # 處理未預期的錯誤
609
+ results.append({
610
+ 'symbol': symbol,
611
+ 'name': symbol,
612
+ 'current_price': 'N/A',
613
+ 'up_probability': 'ERROR',
614
+ 'down_probability': 'ERROR',
615
+ 'sideways_probability': 'ERROR',
616
+ 'confidence': 'ERROR',
617
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
618
+ 'error_message': f'未預期錯誤: {str(e)}'
619
+ })
620
+ progress_messages.append(f"❌ {symbol}: 未預期錯誤")
621
+
622
+ # 寫入 CSV 檔案
623
+ with open(result_file, 'w', newline='', encoding='utf-8-sig') as csvfile:
624
+ fieldnames = ['symbol', 'name', 'current_price', 'up_probability',
625
+ 'down_probability', 'sideways_probability', 'confidence',
626
+ 'analysis_time', 'error_message']
627
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
628
+
629
+ # 寫入標題行
630
+ writer.writerow({
631
+ 'symbol': '股票代號',
632
+ 'name': '股票名稱',
633
+ 'current_price': '當前價格',
634
+ 'up_probability': '上漲機率(%)',
635
+ 'down_probability': '下跌機率(%)',
636
+ 'sideways_probability': '盤整機率(%)',
637
+ 'confidence': '信心度(%)',
638
+ 'analysis_time': '分析時間',
639
+ 'error_message': '錯誤訊息'
640
+ })
641
+
642
+ # 寫入數據
643
+ writer.writerows(results)
644
+
645
+ # 統計結果
646
+ success_count = len([r for r in results if r['error_message'] == ''])
647
+ error_count = len(results) - success_count
648
+
649
+ summary_message = f"""
650
+ 📈 批次分析完成!
651
+
652
+ 📊 **分析統計:**
653
+ - 總計股票數:{len(stock_symbols)}
654
+ - 成功分析:{success_count}
655
+ - 分析失敗:{error_count}
656
+
657
+ 💾 **結果已儲存至:** `{result_file}`
658
+
659
+ ✨ **檔案包含欄位:**
660
+ - 股票代號、股票名稱、當前價格
661
+ - 上漲機率(%)、下跌機率(%)、盤整機率(%)
662
+ - 信心度(%)、分析時間、錯誤訊息
663
+
664
+ 🎯 **可用於進一步分析或投資決策參考!**
665
+ """
666
+
667
+ progress_log = "\n".join(progress_messages)
668
+ return summary_message, progress_log
669
+
670
+ except Exception as e:
671
+ return f"❌ 批次分析過程中發生錯誤:{str(e)}", ""
672
+
673
+ # 創建 Gradio 界面
674
+ with gr.Blocks(title="AI 股票分析師", theme=gr.themes.Soft()) as app:
675
+ gr.Markdown(
676
+ """
677
+ # 📈 AI 股票分析師
678
+
679
+ ### 🤖 使用 Hugging Face 模型進行智能股票分析
680
+
681
+ **✨ 核心功能:**
682
+ - 📊 **完整技術指標**:MA、RSI、MACD、布林通道分析
683
+ - 🧠 **AI 情感分析**:使用 FinBERT 模型分析市場情緒
684
+ - 🎯 **機率預測**:提供上漲/下跌/盤整機率百分比
685
+ - 📈 **智能建議**:根據機率給出個性化投資策略
686
+ - 🖼️ **互動圖表**:動態視覺化技術指標走勢
687
+ - 📁 **批次分析**:一次分析多支股票並匯出CSV報告
688
+
689
+ **🚀 使用方法:** 單支分析輸入股票代碼,批次分析請確保 `StockList.txt` 檔案存在!
690
+ """
691
+ )
692
+
693
+ # 建立分頁
694
+ with gr.Tabs():
695
+ with gr.TabItem("🎯 單支股票分析"):
696
+ with gr.Row():
697
+ with gr.Column(scale=1):
698
+ stock_input = gr.Textbox(
699
+ label="股票代碼",
700
+ placeholder="例如:AAPL, TSLA, 2330.TW",
701
+ value="2330.TW"
702
+ )
703
+ analyze_btn = gr.Button("開始分析", variant="primary", size="lg")
704
+
705
+ status_output = gr.Textbox(
706
+ label="分析狀態",
707
+ lines=2,
708
+ interactive=False
709
+ )
710
+
711
+ with gr.Column(scale=2):
712
+ chart_output = gr.Plot(label="股價走勢圖")
713
+
714
+ prediction_output = gr.Markdown(label="AI 分析報告")
715
+
716
+ # 事件綁定
717
+ analyze_btn.click(
718
+ fn=analyze_stock,
719
+ inputs=[stock_input],
720
+ outputs=[chart_output, status_output, prediction_output]
721
+ )
722
+
723
+ # 範例按鈕
724
+ gr.Examples(
725
+ examples=[
726
+ ["AAPL"],
727
+ ["TSLA"],
728
+ ["2330.TW"],
729
+ ["MSFT"],
730
+ ["GOOGL"]
731
+ ],
732
+ inputs=[stock_input]
733
+ )
734
+
735
+ with gr.TabItem("📊 批次股票分析"):
736
+ gr.Markdown(
737
+ """
738
+ ### 📁 批次分析功能
739
+
740
+ **📋 使用步驟:**
741
+ 1. 確保 `StockList.txt` 檔案存在於專案目錄
742
+ 2. 檔案中每行一個股票代號(如:2330.TW)
743
+ 3. 點擊「開始批次分析」按鈕
744
+ 4. 分析完成後會產生 `StockResult.csv` 檔案
745
+
746
+ **📈 輸出內容:**
747
+ - 股票代號、名稱、當前價格
748
+ - 上漲/下跌/盤整機率(%)
749
+ - 信心度(%)、分析時間
750
+ - 錯誤訊息(如有)
751
+ """
752
+ )
753
+
754
+ with gr.Row():
755
+ batch_analyze_btn = gr.Button(
756
+ "🚀 開始批次分析",
757
+ variant="primary",
758
+ size="lg",
759
+ scale=1
760
+ )
761
+
762
+ with gr.Row():
763
+ with gr.Column(scale=1):
764
+ batch_summary = gr.Markdown(label="📊 分析摘要")
765
+ with gr.Column(scale=1):
766
+ batch_progress = gr.Textbox(
767
+ label="📋 分析進度",
768
+ lines=15,
769
+ interactive=False,
770
+ max_lines=20
771
+ )
772
+
773
+ # 批次分析事件綁定
774
+ batch_analyze_btn.click(
775
+ fn=batch_analyze_stocks,
776
+ inputs=[],
777
+ outputs=[batch_summary, batch_progress]
778
+ )
779
+
780
+ # 啟動應用
781
+ if __name__ == "__main__":
782
+ print("正在啟動 AI 股票分析師...")
783
+
784
+ # 簡化的啟動邏輯
785
+ try:
786
+ if IS_HUGGINGFACE_SPACE:
787
+ # Hugging Face Spaces 環境 - 使用預設配置
788
+ print("在 Hugging Face Spaces 中啟動...")
789
+ app.launch()
790
+ else:
791
+ # 本地環境 - 嘗試多個端口
792
+ print("在本地環境中啟動...")
793
+ ports_to_try = [7860, 7861, 7862, 7863, 7864, 7865]
794
+
795
+ launched = False
796
+ for port in ports_to_try:
797
+ try:
798
+ print(f"嘗試端口 {port}...")
799
+ app.launch(
800
+ share=True,
801
+ server_name="0.0.0.0",
802
+ server_port=port,
803
+ show_error=True,
804
+ quiet=False
805
+ )
806
+ launched = True
807
+ break
808
+ except OSError as e:
809
+ if "port" in str(e).lower():
810
+ print(f"端口 {port} 不可用,嘗試下一個...")
811
+ continue
812
+ else:
813
+ raise e
814
+
815
+ if not launched:
816
+ print("所有預設端口都被佔用,使用隨機端口...")
817
+ app.launch(
818
+ share=True,
819
+ server_name="0.0.0.0",
820
+ server_port=0, # 0 表示自動分配端口
821
+ show_error=True
822
+ )
823
+
824
+ except Exception as e:
825
+ print(f"啟動失敗: {e}")
826
+ print("請檢查端口使用情況或嘗試重新啟動")
827
+ raise e