54justin commited on
Commit
1482fb5
·
verified ·
1 Parent(s): 4178379

Create app_batch.py

Browse files
Files changed (1) hide show
  1. app_batch.py +1089 -0
app_batch.py ADDED
@@ -0,0 +1,1089 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_comprehensive_prediction(self, technical_signals, sentiment, recent_data):
316
+ """生成綜合預測報告"""
317
+ # 計算價格變化
318
+ price_change = ((recent_data['Close'].iloc[-1] - recent_data['Close'].iloc[-5]) / recent_data['Close'].iloc[-5]) * 100
319
+
320
+ # 計算預測機率
321
+ probabilities = self.calculate_prediction_probabilities(technical_signals, sentiment, recent_data)
322
+
323
+ # 確定主要預測方向
324
+ max_prob = max(probabilities['up'], probabilities['down'], probabilities['sideways'])
325
+ if probabilities['up'] == max_prob:
326
+ main_direction = "看多"
327
+ direction_emoji = "📈"
328
+ elif probabilities['down'] == max_prob:
329
+ main_direction = "看空"
330
+ direction_emoji = "📉"
331
+ else:
332
+ main_direction = "盤整"
333
+ direction_emoji = "➡️"
334
+
335
+ # 信心度描述
336
+ confidence = probabilities['confidence']
337
+ if confidence > 0.4:
338
+ confidence_desc = "高信心"
339
+ elif confidence > 0.2:
340
+ confidence_desc = "中等信心"
341
+ else:
342
+ confidence_desc = "低信心"
343
+
344
+ report = f"""
345
+ ## 📊 {self.symbol} AI 分析報告
346
+
347
+ ### 📈 技術面分析:
348
+ {chr(10).join(f"• {signal}" for signal in technical_signals)}
349
+
350
+ ### 💭 市場情感:{sentiment}
351
+
352
+ ### 📊 近期表現:
353
+ - 5日漲跌幅:{price_change:+.2f}%
354
+ - 當前價位:${recent_data['Close'].iloc[-1]:.2f}
355
+
356
+ ### 🤖 AI 預測機率(短期 1-7天):
357
+
358
+ | 方向 | 機率 | 說明 |
359
+ |------|------|------|
360
+ | 📈 **上漲** | **{probabilities['up']:.1f}%** | 股價向上突破的可能性 |
361
+ | 📉 **下跌** | **{probabilities['down']:.1f}%** | 股價向下修正的可能性 |
362
+ | ➡️ **盤整** | **{probabilities['sideways']:.1f}%** | 股價維持震盪的可能性 |
363
+
364
+ ### 🎯 主要預測方向:
365
+ {direction_emoji} **{main_direction}** ({confidence_desc} - {confidence*100:.0f}%)
366
+
367
+ ### 📋 投資建議:
368
+ """
369
+
370
+ # 根據最高機率給出建議
371
+ if probabilities['up'] > 50:
372
+ report += """
373
+ - 💡 **多頭策略**:考慮逢低加碼或持有現有部位
374
+ - 🎯 **目標設定**:關注上方阻力位,設定合理獲利目標
375
+ - 🛡️ **風險管理**:設置止損點保護資本"""
376
+ elif probabilities['down'] > 50:
377
+ report += """
378
+ - 💡 **防守策略**:考慮減碼或等待更佳進場點
379
+ - 🎯 **支撐觀察**:留意下方支撐位是否守住
380
+ - 🛡️ **風險管理**:避免追高,控制倉位大小"""
381
+ else:
382
+ report += """
383
+ - 💡 **中性策略**:保持觀望,等待明確方向訊號
384
+ - 🎯 **區間操作**:可考慮在支撐阻力區間內操作
385
+ - 🛡️ **風險管理**:小部位測試,嚴格執行停損"""
386
+
387
+ report += f"""
388
+
389
+ ### 📅 中期展望(1個月):
390
+ 基於當前技術面和市場情緒分析,建議持續關注:
391
+ - 關鍵技術位:支撐與阻力區間
392
+ - 市場情緒變化:新聞面和資金流向
393
+ - 整體大盤走勢:系統性風險評估
394
+
395
+ ⚠️ **風險提醒**:此分析基於歷史數據和 AI 模型預測,僅供參考。投資有風險,請謹慎評估並做好風險管理!
396
+
397
+ ---
398
+ *預測信心度:{confidence*100:.0f}% | 分析時間:{datetime.now().strftime('%Y-%m-%d %H:%M')}*
399
+ """
400
+
401
+ return report
402
+
403
+ def generate_prediction(self, df, news_sentiment):
404
+ """生成預測分析"""
405
+ if df is None or len(df) < 30:
406
+ return "數據不足,無法進行預測分析"
407
+
408
+ # 獲取最新數據
409
+ latest = df.iloc[-1]
410
+ recent_data = df.tail(20)
411
+
412
+ # 技術分析信號
413
+ technical_signals = []
414
+
415
+ # 價格趋势
416
+ if latest['Close'] > latest['MA20']:
417
+ technical_signals.append("價格在20日均線之上(多頭信號)")
418
+ else:
419
+ technical_signals.append("價格在20日均線之下(空頭信號)")
420
+
421
+ # RSI 分析
422
+ rsi = latest['RSI']
423
+ if rsi > 70:
424
+ technical_signals.append(f"RSI({rsi:.1f}) 超買警訊")
425
+ elif rsi < 30:
426
+ technical_signals.append(f"RSI({rsi:.1f}) 超賣機會")
427
+ else:
428
+ technical_signals.append(f"RSI({rsi:.1f}) 正常範圍")
429
+
430
+ # MACD 分析
431
+ if latest['MACD'] > latest['MACD_signal']:
432
+ technical_signals.append("MACD 呈現多頭排列")
433
+ else:
434
+ technical_signals.append("MACD 呈現空頭排列")
435
+
436
+ # 新聞情感分析
437
+ sentiment_summary = self.analyze_sentiment_summary(news_sentiment)
438
+
439
+ # 綜合預測
440
+ prediction = self.generate_comprehensive_prediction(technical_signals, sentiment_summary, recent_data)
441
+
442
+ return prediction
443
+
444
+ # 創建分析器實例
445
+ analyzer = StockAnalyzer()
446
+
447
+ def analyze_stock(symbol):
448
+ """主要分析函數"""
449
+ if not symbol.strip():
450
+ return None, "請輸入股票代碼", ""
451
+
452
+ # 獲取數據
453
+ result = analyzer.fetch_stock_data(symbol.upper())
454
+ if len(result) == 3:
455
+ success, message, stock_name = result
456
+ else:
457
+ success, message = result
458
+ stock_name = None
459
+
460
+ if not success:
461
+ return None, message, ""
462
+
463
+ # 計算技術指標
464
+ df = analyzer.calculate_technical_indicators()
465
+
466
+ # 創建價格圖表
467
+ fig = go.Figure()
468
+
469
+ # 添加K線圖
470
+ fig.add_trace(go.Candlestick(
471
+ x=df.index,
472
+ open=df['Open'],
473
+ high=df['High'],
474
+ low=df['Low'],
475
+ close=df['Close'],
476
+ name='價格'
477
+ ))
478
+
479
+ # 添加移動平均線
480
+ fig.add_trace(go.Scatter(x=df.index, y=df['MA5'], name='MA5', line=dict(color='orange')))
481
+ fig.add_trace(go.Scatter(x=df.index, y=df['MA20'], name='MA20', line=dict(color='blue')))
482
+
483
+ fig.update_layout(
484
+ title=f'{symbol} 股價走勢與技術指標',
485
+ xaxis_title='日期',
486
+ yaxis_title='價格',
487
+ height=600
488
+ )
489
+
490
+ # 獲取新聞情感
491
+ news_sentiment = analyzer.get_news_sentiment(symbol)
492
+
493
+ # 生成預測
494
+ prediction = analyzer.generate_prediction(df, news_sentiment)
495
+
496
+ return fig, "分析完成!", prediction
497
+
498
+ def create_results_table(results):
499
+ """創建結果表格"""
500
+ if not results:
501
+ return ""
502
+
503
+ # 創建表格 HTML
504
+ table_html = """
505
+ <div style="overflow-x: auto; margin: 20px 0;">
506
+ <table style="width: 100%; border-collapse: collapse; font-family: Arial, sans-serif;">
507
+ <thead>
508
+ <tr style="background-color: #f0f0f0;">
509
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: left;">股票代號</th>
510
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: left;">股票名稱</th>
511
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: right;">當前價格</th>
512
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: right;">上漲機率(%)</th>
513
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: right;">下跌機率(%)</th>
514
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: right;">盤整機率(%)</th>
515
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: right;">信心度(%)</th>
516
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: center;">預測方向</th>
517
+ <th style="border: 1px solid #ddd; padding: 12px; text-align: left;">狀態</th>
518
+ </tr>
519
+ </thead>
520
+ <tbody>
521
+ """
522
+
523
+ for result in results:
524
+ # 判斷預測方向和顏色
525
+ if result['error_message']:
526
+ direction = "❌ 錯誤"
527
+ row_color = "#fff2f2"
528
+ else:
529
+ up_prob = float(result['up_probability'])
530
+ down_prob = float(result['down_probability'])
531
+ sideways_prob = float(result['sideways_probability'])
532
+
533
+ if up_prob > down_prob and up_prob > sideways_prob:
534
+ direction = "📈 看多"
535
+ row_color = "#f0fff0" # 淡綠色
536
+ elif down_prob > up_prob and down_prob > sideways_prob:
537
+ direction = "📉 看空"
538
+ row_color = "#fff0f0" # 淡紅色
539
+ else:
540
+ direction = "➡️ 盤整"
541
+ row_color = "#f8f8f8" # 淡灰色
542
+
543
+ status = "✅ 成功" if not result['error_message'] else f"❌ {result['error_message'][:30]}..."
544
+
545
+ table_html += f"""
546
+ <tr style="background-color: {row_color};">
547
+ <td style="border: 1px solid #ddd; padding: 8px; font-weight: bold;">{result['symbol']}</td>
548
+ <td style="border: 1px solid #ddd; padding: 8px;">{result['name']}</td>
549
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['current_price']}</td>
550
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['up_probability']}</td>
551
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['down_probability']}</td>
552
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['sideways_probability']}</td>
553
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">{result['confidence']}</td>
554
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: center;">{direction}</td>
555
+ <td style="border: 1px solid #ddd; padding: 8px;">{status}</td>
556
+ </tr>
557
+ """
558
+
559
+ table_html += """
560
+ </tbody>
561
+ </table>
562
+ </div>
563
+ """
564
+
565
+ return table_html
566
+
567
+ def create_batch_analysis_charts(results):
568
+ """創建批次分析結果圖表"""
569
+ if not results:
570
+ return None, None, None, None
571
+
572
+ # 過濾出成功分析的結果
573
+ success_results = [r for r in results if r['error_message'] == '']
574
+
575
+ if not success_results:
576
+ return None, None, None, None
577
+
578
+ # 準備數據
579
+ symbols = [r['symbol'] for r in success_results]
580
+ up_probs = [float(r['up_probability']) for r in success_results]
581
+ down_probs = [float(r['down_probability']) for r in success_results]
582
+ sideways_probs = [float(r['sideways_probability']) for r in success_results]
583
+ confidence = [float(r['confidence']) for r in success_results]
584
+
585
+ # 1. 機率比較柱狀圖
586
+ fig_bar = go.Figure()
587
+ fig_bar.add_trace(go.Bar(name='上漲機率', x=symbols, y=up_probs, marker_color='green', opacity=0.8))
588
+ fig_bar.add_trace(go.Bar(name='下跌機率', x=symbols, y=down_probs, marker_color='red', opacity=0.8))
589
+ fig_bar.add_trace(go.Bar(name='盤整機率', x=symbols, y=sideways_probs, marker_color='gray', opacity=0.8))
590
+
591
+ fig_bar.update_layout(
592
+ title='📊 股票預測機率比較',
593
+ xaxis_title='股票代號',
594
+ yaxis_title='機率 (%)',
595
+ barmode='group',
596
+ height=500,
597
+ showlegend=True,
598
+ xaxis_tickangle=-45
599
+ )
600
+
601
+ # 2. 信心度散佈圖
602
+ fig_scatter = go.Figure()
603
+
604
+ # 根據最高機率決定顏色
605
+ colors = []
606
+ for i in range(len(success_results)):
607
+ if up_probs[i] > down_probs[i] and up_probs[i] > sideways_probs[i]:
608
+ colors.append('green') # 看多
609
+ elif down_probs[i] > up_probs[i] and down_probs[i] > sideways_probs[i]:
610
+ colors.append('red') # 看空
611
+ else:
612
+ colors.append('gray') # 盤整
613
+
614
+ fig_scatter.add_trace(go.Scatter(
615
+ x=symbols,
616
+ y=confidence,
617
+ mode='markers+text',
618
+ marker=dict(
619
+ size=[max(prob) for prob in zip(up_probs, down_probs, sideways_probs)],
620
+ sizemode='diameter',
621
+ sizeref=2,
622
+ color=colors,
623
+ opacity=0.7,
624
+ line=dict(width=2, color='white')
625
+ ),
626
+ text=[f"{conf:.1f}%" for conf in confidence],
627
+ textposition="middle center",
628
+ name='信心度'
629
+ ))
630
+
631
+ fig_scatter.update_layout(
632
+ title='🎯 預測信心度分佈 (圓圈大小=最高機率)',
633
+ xaxis_title='股票代號',
634
+ yaxis_title='信心度 (%)',
635
+ height=500,
636
+ xaxis_tickangle=-45
637
+ )
638
+
639
+ # 3. 綜合評分雷達圖 (取前6支股票)
640
+ radar_data = success_results[:6] # 限制顯示數量避免過於擁擠
641
+ fig_radar = go.Figure()
642
+
643
+ categories = ['上漲機率', '信心度', '綜合評分']
644
+
645
+ for i, result in enumerate(radar_data):
646
+ # 計算綜合評分 (上漲機率 * 信心度 / 100)
647
+ composite_score = float(result['up_probability']) * float(result['confidence']) / 100
648
+
649
+ values = [
650
+ float(result['up_probability']),
651
+ float(result['confidence']),
652
+ composite_score
653
+ ]
654
+
655
+ fig_radar.add_trace(go.Scatterpolar(
656
+ r=values + [values[0]], # 閉合雷達圖
657
+ theta=categories + [categories[0]],
658
+ fill='toself',
659
+ name=result['symbol'],
660
+ opacity=0.6
661
+ ))
662
+
663
+ fig_radar.update_layout(
664
+ polar=dict(
665
+ radialaxis=dict(
666
+ visible=True,
667
+ range=[0, 100]
668
+ )
669
+ ),
670
+ title='📈 股票綜合評分雷達圖 (前6支)',
671
+ height=500,
672
+ showlegend=True
673
+ )
674
+
675
+ # 4. 機率分佈餅圖統計
676
+ # 統計各種預測傾向的數量
677
+ bullish_count = sum(1 for r in success_results if float(r['up_probability']) > max(float(r['down_probability']), float(r['sideways_probability'])))
678
+ bearish_count = sum(1 for r in success_results if float(r['down_probability']) > max(float(r['up_probability']), float(r['sideways_probability'])))
679
+ neutral_count = len(success_results) - bullish_count - bearish_count
680
+
681
+ fig_pie = go.Figure(data=[go.Pie(
682
+ labels=['看多股票', '看空股票', '盤整股票'],
683
+ values=[bullish_count, bearish_count, neutral_count],
684
+ marker_colors=['green', 'red', 'gray'],
685
+ textinfo='label+percent+value',
686
+ hovertemplate='<b>%{label}</b><br>數量: %{value}<br>比例: %{percent}<extra></extra>'
687
+ )])
688
+
689
+ fig_pie.update_layout(
690
+ title='🥧 整體市場情緒分佈',
691
+ height=400
692
+ )
693
+
694
+ return fig_bar, fig_scatter, fig_radar, fig_pie
695
+
696
+ def batch_analyze_stocks(stock_input_text):
697
+ """批次分析股票清單"""
698
+ # 檢查輸入是否為空
699
+ if not stock_input_text or not stock_input_text.strip():
700
+ return "❌ 請輸入股票代號!", "", None, None, None, None, ""
701
+
702
+ try:
703
+ # 從文字輸入框解析股票清單
704
+ # 支援多種分隔符:換行、逗號、分號、空格
705
+ import re
706
+ stock_symbols = re.split(r'[,;\s\n]+', stock_input_text.strip())
707
+ stock_symbols = [symbol.strip().upper() for symbol in stock_symbols if symbol.strip()]
708
+
709
+ if not stock_symbols:
710
+ return "❌ 未能解析出有效的股票代號!", "", None, None, None, None, ""
711
+
712
+ # 準備結果列表
713
+ results = []
714
+ progress_messages = []
715
+
716
+ progress_messages.append(f"📊 開始批次分析 {len(stock_symbols)} 支股票...")
717
+
718
+ # 分析每支股票
719
+ for i, symbol in enumerate(stock_symbols, 1):
720
+ progress_messages.append(f"\n🔍 正在分析 ({i}/{len(stock_symbols)}): {symbol}")
721
+
722
+ try:
723
+ # 獲取股票數據
724
+ result = analyzer.fetch_stock_data(symbol.upper())
725
+ if len(result) == 3:
726
+ success, message, stock_name = result
727
+ else:
728
+ success, message = result
729
+ stock_name = symbol
730
+
731
+ if not success:
732
+ # 記錄錯誤
733
+ results.append({
734
+ 'symbol': symbol,
735
+ 'name': stock_name or symbol,
736
+ 'current_price': 'N/A',
737
+ 'up_probability': 'ERROR',
738
+ 'down_probability': 'ERROR',
739
+ 'sideways_probability': 'ERROR',
740
+ 'confidence': 'ERROR',
741
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
742
+ 'error_message': message
743
+ })
744
+ progress_messages.append(f"❌ {symbol}: {message}")
745
+ continue
746
+
747
+ # 計算技術指標
748
+ df = analyzer.calculate_technical_indicators()
749
+ if df is None or len(df) < 30:
750
+ results.append({
751
+ 'symbol': symbol,
752
+ 'name': stock_name or symbol,
753
+ 'current_price': 'N/A',
754
+ 'up_probability': 'ERROR',
755
+ 'down_probability': 'ERROR',
756
+ 'sideways_probability': 'ERROR',
757
+ 'confidence': 'ERROR',
758
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
759
+ 'error_message': '數據不足,無法分析'
760
+ })
761
+ progress_messages.append(f"❌ {symbol}: 數據不足")
762
+ continue
763
+
764
+ # 獲取新聞情感
765
+ news_sentiment = analyzer.get_news_sentiment(symbol)
766
+ sentiment_summary = analyzer.analyze_sentiment_summary(news_sentiment)
767
+
768
+ # 計算預測機率
769
+ recent_data = df.tail(20)
770
+ technical_signals = []
771
+
772
+ # 簡化的技術信號計算
773
+ latest = df.iloc[-1]
774
+ if latest['Close'] > latest['MA20']:
775
+ technical_signals.append("價格在20日均線之上")
776
+ else:
777
+ technical_signals.append("價格在20日均線之下")
778
+
779
+ probabilities = analyzer.calculate_prediction_probabilities(
780
+ technical_signals, sentiment_summary, recent_data
781
+ )
782
+
783
+ # 獲取股票資訊
784
+ stock_info = analyzer.get_stock_info(symbol)
785
+
786
+ # 記錄成功結果
787
+ results.append({
788
+ 'symbol': symbol,
789
+ 'name': stock_info['name'],
790
+ 'current_price': f"{latest['Close']:.2f}" if latest['Close'] else 'N/A',
791
+ 'up_probability': f"{probabilities['up']:.1f}",
792
+ 'down_probability': f"{probabilities['down']:.1f}",
793
+ 'sideways_probability': f"{probabilities['sideways']:.1f}",
794
+ 'confidence': f"{probabilities['confidence']*100:.1f}",
795
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
796
+ 'error_message': ''
797
+ })
798
+
799
+ progress_messages.append(f"✅ {symbol}: 分析完成")
800
+
801
+ except Exception as e:
802
+ # 處理未預期的錯誤
803
+ results.append({
804
+ 'symbol': symbol,
805
+ 'name': symbol,
806
+ 'current_price': 'N/A',
807
+ 'up_probability': 'ERROR',
808
+ 'down_probability': 'ERROR',
809
+ 'sideways_probability': 'ERROR',
810
+ 'confidence': 'ERROR',
811
+ 'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
812
+ 'error_message': f'未預期錯誤: {str(e)}'
813
+ })
814
+ progress_messages.append(f"❌ {symbol}: 未預期錯誤")
815
+
816
+ # 統計結果
817
+ success_count = len([r for r in results if r['error_message'] == ''])
818
+ error_count = len(results) - success_count
819
+
820
+ summary_message = f"""
821
+ 📈 批次分析完成!
822
+
823
+ 📊 **分析統計:**
824
+ - 總計股票數:{len(stock_symbols)}
825
+ - 成功分析:{success_count}
826
+ - 分析失敗:{error_count}
827
+
828
+ � **圖表已生成:**
829
+ - 📊 機率比較柱狀圖
830
+ - 🎯 信心度散佈圖
831
+ - 📈 綜合評分雷達圖
832
+ - 🥧 市場情緒餅圖
833
+
834
+ 🎯 **請查看下方圖表進行投資決策分析!**
835
+ """
836
+
837
+ progress_log = "\n".join(progress_messages)
838
+
839
+ # 創建圖表和結果表格
840
+ chart_bar, chart_scatter, chart_radar, chart_pie = create_batch_analysis_charts(results)
841
+ results_table = create_results_table(results)
842
+
843
+ return summary_message, progress_log, chart_bar, chart_scatter, chart_radar, chart_pie, results_table
844
+
845
+ except Exception as e:
846
+ return f"❌ 批次分析過程中發生錯誤:{str(e)}", "", None, None, None, None, ""
847
+
848
+ # 創建 Gradio 界面
849
+ with gr.Blocks(title="AI 股票分析師", theme=gr.themes.Soft()) as app:
850
+ gr.Markdown(
851
+ """
852
+ # 📈 AI 股票分析師
853
+
854
+ ### 🤖 使用 Hugging Face 模型進行智能股票分析
855
+
856
+ **✨ 核心功能:**
857
+ - 📊 **完整技術指標**:MA、RSI、MACD、布林通道分析
858
+ - 🧠 **AI 情感分析**:使用 FinBERT 模型分析市場情緒
859
+ - 🎯 **機率預測**:提供上漲/下跌/盤整機率百分比
860
+ - 📈 **智能建議**:根據機率給出個性化投資策略
861
+ - 🖼️ **互動圖表**:動態視覺化技術指標走勢
862
+ - 📁 **批次分析**:一次分析多支股票並匯出CSV報告
863
+
864
+ **🚀 使用方法:** 單支分析輸入股票代碼,批次分析直接在文字框中輸入多個股票代號!
865
+ """
866
+ )
867
+
868
+ # 建立分頁
869
+ with gr.Tabs():
870
+ with gr.TabItem("🎯 單支股票分析"):
871
+ with gr.Row():
872
+ with gr.Column(scale=1):
873
+ stock_input = gr.Textbox(
874
+ label="股票代碼",
875
+ placeholder="例如:AAPL, TSLA, 2330.TW",
876
+ value="2330.TW"
877
+ )
878
+ analyze_btn = gr.Button("開始分析", variant="primary", size="lg")
879
+
880
+ status_output = gr.Textbox(
881
+ label="分析狀態",
882
+ lines=2,
883
+ interactive=False
884
+ )
885
+
886
+ with gr.Column(scale=2):
887
+ chart_output = gr.Plot(label="股價走勢圖")
888
+
889
+ prediction_output = gr.Markdown(label="AI 分析報告")
890
+
891
+ # 事件綁定
892
+ analyze_btn.click(
893
+ fn=analyze_stock,
894
+ inputs=[stock_input],
895
+ outputs=[chart_output, status_output, prediction_output]
896
+ )
897
+
898
+ # 範例按鈕
899
+ gr.Examples(
900
+ examples=[
901
+ ["AAPL"],
902
+ ["TSLA"],
903
+ ["2330.TW"],
904
+ ["MSFT"],
905
+ ["GOOGL"]
906
+ ],
907
+ inputs=[stock_input]
908
+ )
909
+
910
+ with gr.TabItem("📊 批次股票分析"):
911
+ gr.Markdown(
912
+ """
913
+ ### 📁 批次分析功能
914
+
915
+ **📋 使用方式:**
916
+ 1. 在下方輸入框中輸入多個股票代號
917
+ 2. 支援多種分隔方式:換行、逗號、分號、空格
918
+ 3. 點擊「開始批次分析」按鈕
919
+ 4. 查看即時互動圖表分析結果
920
+
921
+ **📈 輸出內容:**
922
+ - 📊 機率比較柱狀圖:直觀對比各股票預測機率
923
+ - 🎯 信心度散佈圖:顯示預測可靠性分佈
924
+ - 📈 綜合評分雷達圖:多維度股票評分比較
925
+ - 🥧 市場情緒餅圖:整體多空情緒統計
926
+ - 📋 詳細結果表格:完整數據一覽
927
+ """
928
+ )
929
+
930
+ # 股票代號輸入區
931
+ with gr.Row():
932
+ with gr.Column(scale=3):
933
+ stock_input_batch = gr.Textbox(
934
+ label="📝 股票代號清單",
935
+ placeholder="""請輸入多個股票代號,支援多種分隔方式:
936
+
937
+ • 換行分隔:
938
+ 2330.TW
939
+ 2317.TW
940
+ 1303.TW
941
+
942
+ • 逗號分隔:2330.TW, 2317.TW, 1303.TW
943
+
944
+ • 空格分隔:2330.TW 2317.TW 1303.TW
945
+
946
+ • 混合分隔:2330.TW, 2317.TW
947
+ 1303.TW; AAPL TSLA""",
948
+ lines=8,
949
+ value="2330.TW\n2317.TW\n1303.TW\n0050.TW"
950
+ )
951
+ with gr.Column(scale=1):
952
+ batch_analyze_btn = gr.Button(
953
+ "🚀 開始批次分析",
954
+ variant="primary",
955
+ size="lg"
956
+ )
957
+
958
+ # 快速範例按鈕
959
+ gr.Markdown("**🚀 快速範例:**")
960
+
961
+ example_tw_btn = gr.Button("🇹🇼 台股熱門", size="sm")
962
+ example_us_btn = gr.Button("🇺🇸 美股科技", size="sm")
963
+ example_etf_btn = gr.Button("📈 熱門ETF", size="sm")
964
+ clear_btn = gr.Button("🗑️ 清空", size="sm")
965
+
966
+ gr.Markdown(
967
+ """
968
+ **💡 支援格式:**
969
+ - 換行分隔
970
+ - 逗號分隔
971
+ - 空格/分號分隔
972
+ - 混合分隔
973
+ """
974
+ )
975
+
976
+ with gr.Row():
977
+ with gr.Column(scale=1):
978
+ batch_summary = gr.Markdown(label="📊 分析摘要")
979
+ with gr.Column(scale=1):
980
+ batch_progress = gr.Textbox(
981
+ label="📋 分析進度",
982
+ lines=10,
983
+ interactive=False,
984
+ max_lines=15
985
+ )
986
+
987
+ # 圖表顯示區域
988
+ gr.Markdown("## 📈 視覺化分析結果")
989
+
990
+ with gr.Row():
991
+ with gr.Column(scale=1):
992
+ chart_probability = gr.Plot(label="📊 股票預測機率比較")
993
+ with gr.Column(scale=1):
994
+ chart_confidence = gr.Plot(label="🎯 預測信心度分佈")
995
+
996
+ with gr.Row():
997
+ with gr.Column(scale=1):
998
+ chart_radar = gr.Plot(label="📈 綜合評分雷達圖")
999
+ with gr.Column(scale=1):
1000
+ chart_sentiment = gr.Plot(label="🥧 整體市場情緒分佈")
1001
+
1002
+ # 詳細結果表格
1003
+ gr.Markdown("## 📋 詳細分析結果")
1004
+ results_table = gr.HTML(label="分析結果表格")
1005
+
1006
+ # 快速範例按鈕事件綁定
1007
+ example_tw_btn.click(
1008
+ lambda: "2330.TW\n2317.TW\n2454.TW\n2882.TW\n6505.TW\n2303.TW",
1009
+ outputs=[stock_input_batch]
1010
+ )
1011
+
1012
+ example_us_btn.click(
1013
+ lambda: "AAPL\nMSFT\nGOOGL\nTSLA\nNVDA\nAMZN",
1014
+ outputs=[stock_input_batch]
1015
+ )
1016
+
1017
+ example_etf_btn.click(
1018
+ lambda: "0050.TW\n0056.TW\nVTI\nVOO\nQQQ\nSPY",
1019
+ outputs=[stock_input_batch]
1020
+ )
1021
+
1022
+ clear_btn.click(
1023
+ lambda: "",
1024
+ outputs=[stock_input_batch]
1025
+ )
1026
+
1027
+ # 批次分析事件綁定
1028
+ batch_analyze_btn.click(
1029
+ fn=batch_analyze_stocks,
1030
+ inputs=[stock_input_batch],
1031
+ outputs=[
1032
+ batch_summary,
1033
+ batch_progress,
1034
+ chart_probability,
1035
+ chart_confidence,
1036
+ chart_radar,
1037
+ chart_sentiment,
1038
+ results_table
1039
+ ]
1040
+ )
1041
+
1042
+ # 啟動應用
1043
+ if __name__ == "__main__":
1044
+ print("正在啟動 AI 股票分析師...")
1045
+
1046
+ # 簡化的啟動邏輯
1047
+ try:
1048
+ if IS_HUGGINGFACE_SPACE:
1049
+ # Hugging Face Spaces 環境 - 使用預設配置
1050
+ print("在 Hugging Face Spaces 中啟動...")
1051
+ app.launch()
1052
+ else:
1053
+ # 本地環境 - 嘗試多個端口
1054
+ print("在本地環境中啟動...")
1055
+ ports_to_try = [7860, 7861, 7862, 7863, 7864, 7865]
1056
+
1057
+ launched = False
1058
+ for port in ports_to_try:
1059
+ try:
1060
+ print(f"嘗試端口 {port}...")
1061
+ app.launch(
1062
+ share=True,
1063
+ server_name="0.0.0.0",
1064
+ server_port=port,
1065
+ show_error=True,
1066
+ quiet=False
1067
+ )
1068
+ launched = True
1069
+ break
1070
+ except OSError as e:
1071
+ if "port" in str(e).lower():
1072
+ print(f"端口 {port} 不可用,嘗試下一個...")
1073
+ continue
1074
+ else:
1075
+ raise e
1076
+
1077
+ if not launched:
1078
+ print("所有預設端口都被佔用,使用隨機端口...")
1079
+ app.launch(
1080
+ share=True,
1081
+ server_name="0.0.0.0",
1082
+ server_port=0, # 0 表示自動分配端口
1083
+ show_error=True
1084
+ )
1085
+
1086
+ except Exception as e:
1087
+ print(f"啟動失敗: {e}")
1088
+ print("請檢查端口使用情況或嘗試重新啟動")
1089
+ raise e