Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -13,6 +13,9 @@ import re
|
|
| 13 |
from bs4 import BeautifulSoup
|
| 14 |
import requests
|
| 15 |
|
|
|
|
|
|
|
|
|
|
| 16 |
# 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
|
| 17 |
TAIWAN_STOCKS = {
|
| 18 |
'元大台灣50': '0050.TW', # 新增
|
|
@@ -1476,57 +1479,135 @@ def update_comparison_analysis(selected_stocks, period):
|
|
| 1476 |
[dash.dependencies.Input('stock-dropdown', 'value')]
|
| 1477 |
)
|
| 1478 |
def update_sentiment_analysis(selected_stock):
|
| 1479 |
-
|
| 1480 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1481 |
|
| 1482 |
-
# 建立情緒指標圓形圖
|
| 1483 |
gauge_fig = go.Figure(go.Indicator(
|
| 1484 |
-
mode = "gauge+number
|
| 1485 |
-
value =
|
| 1486 |
domain = {'x': [0, 1], 'y': [0, 1]},
|
| 1487 |
title = {'text': "市場情緒指數"},
|
| 1488 |
-
delta = {'reference': 50},
|
| 1489 |
gauge = {
|
| 1490 |
-
'axis': {'range': [
|
| 1491 |
-
'bar': {'color': "
|
| 1492 |
'steps': [
|
| 1493 |
-
{'range': [0, 30], 'color': "
|
| 1494 |
-
{'range': [30, 70], 'color': "
|
| 1495 |
-
{'range': [70, 100], 'color': "
|
| 1496 |
],
|
| 1497 |
-
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
|
|
|
| 1502 |
}
|
| 1503 |
))
|
| 1504 |
|
| 1505 |
gauge_fig.update_layout(height=200, margin=dict(l=20, r=20, t=40, b=20))
|
| 1506 |
|
| 1507 |
-
|
| 1508 |
-
stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
|
| 1509 |
-
|
| 1510 |
-
news_items = [
|
| 1511 |
-
f"📈 {stock_name}獲外資調升目標價,看好後續發展前景",
|
| 1512 |
-
f"💼 法人預期{stock_name}下季營收將較上季成長5-10%",
|
| 1513 |
-
f"🌐 國際市場波動對{stock_name}影響有限,基本面穩健",
|
| 1514 |
-
f"⚡ 產業景氣回溫,{stock_name}受惠程度值得關注",
|
| 1515 |
-
f"📊 技術面顯示{stock_name}突破關鍵壓力,短線偏多"
|
| 1516 |
-
]
|
| 1517 |
-
|
| 1518 |
-
news_content = html.Div([
|
| 1519 |
-
html.P(news, style={
|
| 1520 |
-
'margin': '8px 0',
|
| 1521 |
-
'padding': '8px',
|
| 1522 |
-
'background': '#f8f9fa',
|
| 1523 |
-
'border-radius': '5px',
|
| 1524 |
-
'border-left': '3px solid #17a2b8',
|
| 1525 |
-
'font-size': '13px'
|
| 1526 |
-
}) for news in news_items[:3] # 顯示前3條
|
| 1527 |
-
])
|
| 1528 |
-
|
| 1529 |
-
return dcc.Graph(figure=gauge_fig), news_content
|
| 1530 |
|
| 1531 |
# 在 Colab 中執行的設定
|
| 1532 |
if __name__ == '__main__':
|
|
|
|
| 13 |
from bs4 import BeautifulSoup
|
| 14 |
import requests
|
| 15 |
|
| 16 |
+
# 引入 BERT 預測模組
|
| 17 |
+
from Bert_predict import predict_sentiment
|
| 18 |
+
|
| 19 |
# 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
|
| 20 |
TAIWAN_STOCKS = {
|
| 21 |
'元大台灣50': '0050.TW', # 新增
|
|
|
|
| 1479 |
[dash.dependencies.Input('stock-dropdown', 'value')]
|
| 1480 |
)
|
| 1481 |
def update_sentiment_analysis(selected_stock):
|
| 1482 |
+
stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
|
| 1483 |
+
|
| 1484 |
+
# --- 修改部分:從 CSV 檔案讀取新聞 ---
|
| 1485 |
+
# 假設 CSV 檔案名稱是 'news_YYYY-MM-DD.csv' 並且與主程式在同一目錄
|
| 1486 |
+
# 您需要確保新聞檔案的命名規則與此一致,或者修改檔案名稱的獲取方式
|
| 1487 |
+
today_str = datetime.now().strftime('%Y-%m-%d') # 獲取今天的日期
|
| 1488 |
+
news_csv_filename = f'news_{today_str}.csv' # 例如: news_2025-09-12.csv
|
| 1489 |
+
|
| 1490 |
+
news_items_content = []
|
| 1491 |
+
try:
|
| 1492 |
+
# 檢查檔案是否存在
|
| 1493 |
+
if not os.path.exists(news_csv_filename):
|
| 1494 |
+
print(f"警告:新聞檔案 '{news_csv_filename}' 不存在。將使用預設新聞。")
|
| 1495 |
+
# 如果檔案不存在,可以選擇顯示錯誤訊息或使用預設模擬新聞
|
| 1496 |
+
news_items_content = [
|
| 1497 |
+
f"⚠️ 無法載入新聞檔案 '{news_csv_filename}',顯示預設內容。",
|
| 1498 |
+
f"📈 {stock_name} 股價受到市場關注。",
|
| 1499 |
+
f"📉 投資者情緒趨於謹慎。",
|
| 1500 |
+
]
|
| 1501 |
+
else:
|
| 1502 |
+
news_df = pd.read_csv(news_csv_filename)
|
| 1503 |
+
# 確保 CSV 檔案中存在 '內容' 欄位
|
| 1504 |
+
if '內容' in news_df.columns:
|
| 1505 |
+
news_items_content = news_df['內容'].tolist()
|
| 1506 |
+
else:
|
| 1507 |
+
print(f"警告:CSV 檔案 '{news_csv_filename}' 中未找到 '內容' 欄位。")
|
| 1508 |
+
news_items_content = [
|
| 1509 |
+
f"⚠️ CSV 檔案 '{news_csv_filename}' 格式錯誤,未找到 '內容' 欄位。",
|
| 1510 |
+
f"📈 {stock_name} 股價動態。",
|
| 1511 |
+
f"📉 市場消息更新。",
|
| 1512 |
+
]
|
| 1513 |
+
except FileNotFoundError:
|
| 1514 |
+
print(f"警告:新聞檔案 '{news_csv_filename}' 讀取時發生 FileNotFoundError。")
|
| 1515 |
+
news_items_content = [
|
| 1516 |
+
f"⚠️ 讀取新聞檔案 '{news_csv_filename}' 時發生錯誤。",
|
| 1517 |
+
f"📈 {stock_name} 股價分析。",
|
| 1518 |
+
f"📉 投資者情緒參考。",
|
| 1519 |
+
]
|
| 1520 |
+
except Exception as e:
|
| 1521 |
+
print(f"讀取 CSV 檔案 '{news_csv_filename}' 時發生未預期錯誤:{e}")
|
| 1522 |
+
news_items_content = [
|
| 1523 |
+
f"⚠️ 讀取新聞檔案 '{news_csv_filename}' 時發生錯誤:{e}",
|
| 1524 |
+
f"📈 {stock_name} 股價資訊。",
|
| 1525 |
+
f"📉 市場趨勢分析。",
|
| 1526 |
+
]
|
| 1527 |
+
|
| 1528 |
+
# --- 接著對讀取的新聞內容進行情緒分析 ---
|
| 1529 |
+
total_sentiment_score = 0
|
| 1530 |
+
analyzed_news_html = []
|
| 1531 |
+
sentiment_mapping = {'negative': 0, 'neutral': 50, 'positive': 100} # BERT 輸出的映射
|
| 1532 |
+
|
| 1533 |
+
if not news_items_content: # 如果 news_items_content 為空
|
| 1534 |
+
analyzed_news_html.append(html.P("目前沒有新聞可供分析。", style={'font-style': 'italic'}))
|
| 1535 |
+
avg_sentiment_score = 50 # 預設為中性
|
| 1536 |
+
else:
|
| 1537 |
+
for news in news_items_content:
|
| 1538 |
+
# 使用 BERT 模型進行預測
|
| 1539 |
+
# predict_sentiment 應返回 (sentiment_label, probability_score)
|
| 1540 |
+
# 例如:('positive', 0.95)
|
| 1541 |
+
sentiment_label, probability_score = predict_sentiment(news)
|
| 1542 |
+
|
| 1543 |
+
# 將情緒標籤轉換為數值分數
|
| 1544 |
+
numeric_score = sentiment_mapping.get(sentiment_label, 50) # 預設為中性
|
| 1545 |
+
|
| 1546 |
+
total_sentiment_score += numeric_score
|
| 1547 |
+
|
| 1548 |
+
# 設置顯示顏色和表情符號
|
| 1549 |
+
sentiment_emoji = '⚪'
|
| 1550 |
+
sentiment_color = 'gray'
|
| 1551 |
+
if sentiment_label == 'positive':
|
| 1552 |
+
sentiment_emoji = '🟢'
|
| 1553 |
+
sentiment_color = 'green'
|
| 1554 |
+
elif sentiment_label == 'neutral':
|
| 1555 |
+
sentiment_emoji = '🟡'
|
| 1556 |
+
sentiment_color = 'orange'
|
| 1557 |
+
elif sentiment_label == 'negative':
|
| 1558 |
+
sentiment_emoji = '🔴'
|
| 1559 |
+
sentiment_color = 'red'
|
| 1560 |
+
|
| 1561 |
+
# 建立新聞摘要的 HTML 元素
|
| 1562 |
+
analyzed_news_html.append(
|
| 1563 |
+
html.P([
|
| 1564 |
+
html.Span(f"{sentiment_emoji} ", style={'font-size': '1.2em'}),
|
| 1565 |
+
html.A(news, href="#", style={
|
| 1566 |
+
'color': sentiment_color,
|
| 1567 |
+
'text-decoration': 'none',
|
| 1568 |
+
'font-weight': 'bold'
|
| 1569 |
+
}),
|
| 1570 |
+
html.Br(),
|
| 1571 |
+
html.Small(f"情緒: {sentiment_label}, 信賴度: {probability_score:.2%}", style={'margin-left': '20px', 'color': 'gray'})
|
| 1572 |
+
], style={
|
| 1573 |
+
'margin': '8px 0',
|
| 1574 |
+
'padding': '8px',
|
| 1575 |
+
'background': '#f8f9fa',
|
| 1576 |
+
'border-radius': '5px',
|
| 1577 |
+
'border-left': f'3px solid {sentiment_color}',
|
| 1578 |
+
'font-size': '13px'
|
| 1579 |
+
})
|
| 1580 |
+
)
|
| 1581 |
+
|
| 1582 |
+
# 計算平均情緒分數
|
| 1583 |
+
avg_sentiment_score = total_sentiment_score / len(news_items_content)
|
| 1584 |
|
| 1585 |
+
# --- 建立情緒指標圓形圖 (Gauge Chart) ---
|
| 1586 |
gauge_fig = go.Figure(go.Indicator(
|
| 1587 |
+
mode = "gauge+number",
|
| 1588 |
+
value = avg_sentiment_score,
|
| 1589 |
domain = {'x': [0, 1], 'y': [0, 1]},
|
| 1590 |
title = {'text': "市場情緒指數"},
|
|
|
|
| 1591 |
gauge = {
|
| 1592 |
+
'axis': {'range': [0, 100], 'tickvals': [0, 25, 50, 75, 100], 'ticktext': ['極度負面', '', '中性', '', '極度正面']},
|
| 1593 |
+
'bar': {'color': "#667eea"}, # 預設的 bar 顏色,可選
|
| 1594 |
'steps': [
|
| 1595 |
+
{'range': [0, 30], 'color': "#e74c3c"}, # 紅色 (負面)
|
| 1596 |
+
{'range': [30, 70], 'color': "#f1c40f"}, # 黃色 (中性)
|
| 1597 |
+
{'range': [70, 100], 'color': "#2ecc71"} # 綠色 (正面)
|
| 1598 |
],
|
| 1599 |
+
# 您可以在這裡調整 threshold 的設定,如果需要的話
|
| 1600 |
+
# 'threshold': {
|
| 1601 |
+
# 'line': {'color': "red", 'width': 4},
|
| 1602 |
+
# 'thickness': 0.75,
|
| 1603 |
+
# 'value': 75 # 例如,設定一個門檻值
|
| 1604 |
+
# }
|
| 1605 |
}
|
| 1606 |
))
|
| 1607 |
|
| 1608 |
gauge_fig.update_layout(height=200, margin=dict(l=20, r=20, t=40, b=20))
|
| 1609 |
|
| 1610 |
+
return dcc.Graph(figure=gauge_fig), html.Div(analyzed_news_html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1611 |
|
| 1612 |
# 在 Colab 中執行的設定
|
| 1613 |
if __name__ == '__main__':
|