Spaces:
Sleeping
Sleeping
| 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 分析功能將停用") | |
| 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 | |
| } | |
| 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 | |
| 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}") | |
| 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) |