ragline / main.py
Kuomin62's picture
Update main.py
24cc454 verified
import os
import uvicorn
import requests
import json
import pandas as pd
import numpy as np
import yfinance as yf
import httpx
import tempfile
from fastapi import FastAPI, Request, Header, BackgroundTasks, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage, AudioSendMessage
import google.generativeai as genai # 修正導入語句
# --- 股票分析相關程式碼 (yfinance) ---
TAIWAN_STOCKS = {
'元大台灣50': '0050.TW',
'台積電': '2330.TW',
'聯發科': '2454.TW',
'鴻海': '2317.TW',
'台達電': '2308.TW',
'廣達': '2382.TW',
'富邦金': '2881.TW',
'中信金': '2891.TW',
'國泰金': '2882.TW',
'聯電': '2303.TW',
'中華電': '2412.TW',
'玉山金': '2884.TW',
'兆豐金': '2886.TW',
'日月光投控': '3711.TW',
'華碩': '2357.TW',
'統一': '1216.TW',
'元大金': '2885.TW',
'智邦': '2345.TW',
'緯創': '3231.TW',
'聯詠': '3034.TW',
'第一金': '2892.TW',
'瑞昱': '2379.TW',
'緯穎': '6669.TWO',
'永豐金': '2890.TW',
'合庫金': '5880.TW',
'華南金': '2880.TW',
'台光電': '2383.TW',
'世芯-KY': '3661.TWO',
'奇鋐': '3017.TW',
'凱基金': '2883.TW',
'大立光': '3008.TW',
'長榮': '2603.TW',
'光寶科': '2301.TW',
'中鋼': '2002.TW',
'中租-KY': '5871.TW',
'國巨': '2327.TW',
'台新金': '2887.TW',
'上海商銀': '5876.TW',
'台泥': '1101.TW',
'台灣大': '3045.TW',
'和碩': '4938.TW',
'遠傳': '4904.TW',
'和泰車': '2207.TW',
'研華': '2395.TW',
'台塑': '1301.TW',
'統一超': '2912.TW',
'藥華藥': '6446.TWO',
'南亞': '1303.TW',
'陽明': '2609.TW',
'萬海': '2615.TW',
'台塑化': '6505.TW',
'慧洋-KY': '2637.TW',
'上銀': '2049.TW',
'南亞科': '2408.TW',
'旺宏': '2337.TW',
'譜瑞-KY': '4966.TWO',
'貿聯-KY': '3665.TW',
'騰雲': '6870.TWO',
'穩懋': '3105.TWO'
}
def get_stock_data(symbol, period='1y'):
"""獲取股票資料"""
try:
stock = yf.Ticker(symbol)
data = stock.history(period=period)
if data.empty:
# Fallback for empty data
stock = yf.Ticker('^TWII')
data = stock.history(period=period)
return data
except Exception as e:
print(f"Error getting stock data for {symbol}: {e}")
return pd.DataFrame()
def calculate_technical_indicators(df):
"""計算技術指標"""
if df.empty:
return df
df['MA5'] = df['Close'].rolling(window=5).mean()
df['MA20'] = df['Close'].rolling(window=20).mean()
delta = df['Close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
rs = gain / loss
df['RSI'] = 100 - (100 / (1 + rs))
return df
def generate_analysis_report(data, stock_name):
"""根據 yfinance 資料產生技術分析報告文字"""
if data.empty or data.iloc[-1].isnull().any():
return f"抱歉,無法取得 {stock_name} 的技術指標資料。"
last_row = data.iloc[-1]
if last_row['RSI'] > 70:
rsi_comment = "RSI指標顯示處於超買區,可能有回檔風險。"
elif last_row['RSI'] < 30:
rsi_comment = "RSI指標顯示處於超賣區,可能有反彈機會。"
else:
rsi_comment = "RSI指標處於中性區間。"
report = f"📈 {stock_name} 技術指標分析\n"
report += f"🗓️ 最新收盤價: ${last_row['Close']:.2f}\n"
report += f"🚀 RSI: {last_row['RSI']:.2f}\n"
report += f"💬 趨勢評估:\n{rsi_comment}\n"
return report
def generate_market_outlook(data, stock_name):
"""根據技術指標生成市場展望與投資建議"""
if data.empty or data.iloc[-1].isnull().any():
return ""
last_row = data.iloc[-1]
outlook_text = f"---\n📊 **市場展望與投資建議**\n"
if last_row['RSI'] > 70:
outlook_text += "🔴 技術面觀察:RSI 指標顯示股價處於超買區,短期內可能有回檔壓力,建議密切關注。\n"
elif last_row['RSI'] < 30:
outlook_text += "🟢 技術面觀察:RSI 指標顯示股價處於超賣區,短期可能出現反彈,需留意支撐力道。\n"
else:
outlook_text += "🟡 技術面觀察:RSI 指標處於中性區間,顯示目前市場趨勢不明顯,可能進入盤整階段。\n"
if last_row['MA5'] > last_row['MA20']:
outlook_text += "📈 趨勢分析:短期均線(MA5)位於中期均線(MA20)上方,短期趨勢偏多。\n"
else:
outlook_text += "📉 趨勢分析:短期均線(MA5)位於中期均線(MA20)下方,短期趨勢偏空。\n"
outlook_text += "\n💡 **綜合建議**:以上為純技術指標分析,僅供參考,不構成任何投資建議。投資決策應搭配公司基本面、產業前景等進行綜合判斷。"
return outlook_text
# --- Gemini AI 相關設定 ---
gemini_client = None
system_instruction = "你是投信分析師,請使用繁體中文2000字以內,分項說明公司股市價量表現、融資融卷、內外資進出及財務資訊,並分析近期公司股市展望給投資人具體的專業建議!"
generation_config = {
"max_output_tokens": 5000,
"temperature": 0.1,
"top_p": 0.2,
}
working_status = os.getenv("DEFALUT_TALKING", default="true").lower() == "true"
# --- FastAPI & Line Bot 初始化 ---
app = FastAPI()
TARGET_URL = "https://huggingface.co/spaces/AlanRex/AITEST"
line_bot_api = None
line_handler = None
# 設定 CORS,允許跨域請求
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def initialize_services():
"""初始化所有外部服務"""
global line_bot_api, line_handler, gemini_client
# 初始化 Line Bot
channel_access_token = os.getenv("CHANNEL_ACCESS_TOKEN")
channel_secret = os.getenv("CHANNEL_SECRET")
if channel_access_token and channel_secret:
line_bot_api = LineBotApi(channel_access_token)
line_handler = WebhookHandler(channel_secret)
print("Line Bot API 已設定")
else:
print("警告: 未找到 Line Bot 環境變數")
# 初始化 Google Gemini
google_api_key = os.getenv("GOOGLE_API_KEY")
if google_api_key:
genai.configure(api_key=google_api_key)
gemini_client = genai # 使用模組本身作為客戶端
print("Google GenAI Client 已設定")
else:
print("警告: 未找到 GOOGLE_API_KEY 環境變數,AI 分析功能將停用")
@app.get("/")
def root():
return {
"title": "Integrated Stock Analysis Line Bot",
"status": "running",
"line_bot_configured": line_bot_api is not None,
"gemini_ai_configured": gemini_client is not None
}
@app.post("/webhook")
async def webhook(
request: Request,
background_tasks: BackgroundTasks,
x_line_signature=Header(None)
):
if not line_handler:
raise HTTPException(status_code=500, detail="Line Bot not configured")
body = await request.body()
try:
background_tasks.add_task(
line_handler.handle,
body.decode("utf-8"),
x_line_signature
)
except InvalidSignatureError:
raise HTTPException(status_code=400, detail="Invalid signature")
return "ok"
def setup_message_handler():
if not line_handler:
return
@line_handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
user_message = event.message.text.strip()
# 處理 podcast 指令
if user_message.lower() == "podcast":
audio_url = "https://huggingface.co/spaces/tkkbbo332/stockline/resolve/main/podcast.mp3"
line_bot_api.reply_message(
event.reply_token,
AudioSendMessage(original_content_url=audio_url, duration=310000)
)
return
response_text = None
# 嘗試進行股票名稱分析
stock_symbol = None
stock_name = None
for name, symbol in TAIWAN_STOCKS.items():
# 檢查名稱或代號 (例如 '2330' in '2330.TW')
if name in user_message or symbol.split('.')[0] in user_message:
stock_symbol = symbol
stock_name = name
break
if stock_symbol:
stock_data = get_stock_data(stock_symbol, period='6mo')
data_with_indicators = calculate_technical_indicators(stock_data)
report_part = generate_analysis_report(data_with_indicators, stock_name)
outlook_part = generate_market_outlook(data_with_indicators, stock_name)
response_text = report_part + outlook_part
# 如果沒有匹配,回覆預設訊息與網址
if response_text is None:
response_text = (
f"無法識別您的指令。\n\n"
f"👉 輸入【股票名稱】(如: 台積電) 查詢即時技術指標。\n\n"
f"您也可以試試輸入 'podcast'。\n\n"
f"🔗 更多功能請訪問:\nhttps://huggingface.co/spaces/AlanRex/AITEST"
)
try:
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=response_text)
)
except Exception as e:
print(f"回覆訊息時出錯: {e}")
@app.on_event("startup")
async def startup_event():
print("正在初始化外部服務...")
initialize_services()
setup_message_handler()
print("服務初始化完成,應用程式啟動。")
if __name__ == "__main__":
port = int(os.getenv("PORT", 8000))
# Uvicorn 8000 port
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=False)