import os import datetime as dt import yfinance as yf import traceback import json from typing import Union, Dict import gradio as gr from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage, AIMessage from ta.momentum import RSIIndicator, StochasticOscillator from ta.trend import MACD from ta.volume import volume_weighted_average_price import pandas as pd import dotenv dotenv.load_dotenv() # --- 新版 Prompt(改為人類可讀的非 JSON 輸出) --- FUNDAMENTAL_ANALYST_PROMPT = """ You are a fundamental analyst specializing in evaluating company (whose symbol is {company}) performance based on stock prices, technical indicators, financial metrics, recent news, industry trends, competitor positioning, and financial ratios. Your task is to provide a comprehensive summary. You have access to the following tools: 1. **get_stock_prices**: Retrieves stock price data and technical indicators. 2. **get_financial_metrics**: Retrieves key financial metrics and financial ratios. 3. **get_financial_news**: Retrieves the latest financial news related to the stock. 4. **get_industry_data** *(if available)*: Retrieves industry trends and competitive positioning information. --- ### Your Task: 1. Use the provided stock symbol to query the tools. 2. Analyze the following areas in sequence: - **Stock price movements and technical indicators**: Examine recent price trends, volatility, and signals from RSI, MACD, VWAP, and other indicators. - **Financial health and key financial ratios**: Assess profitability, liquidity, solvency, and operational efficiency using metrics such as: - Profitability Ratios: Gross Profit Margin, Net Profit Margin, Operating Profit Margin - Liquidity Ratios: Current Ratio, Quick Ratio - Solvency Ratios: Debt-to-Equity Ratio, Interest Coverage Ratio - Efficiency Ratios: Inventory Turnover, Accounts Receivable Turnover - Market Ratios: Price-to-Earnings Ratio (P/E), Price-to-Book Ratio (P/B) - **Recent news and market sentiment**: Identify significant events or trends impacting the company's market perception. - **Industry analysis**: Evaluate the industry’s growth trends, technological advancements, and regulatory environment. Identify how the industry is evolving and how it affects the target company. - **Competitor analysis**: Compare the target company with key competitors in terms of market share, financial health, and growth potential. 3. Provide a concise and structured summary covering all sections, ensuring each area has actionable insights. --- ### Output Format: 以下請用繁體中文輸出,且以「易讀文字段落/清單」方式呈現 (非 JSON): 請根據以下結構,撰寫一份詳細的分析報告: 1. 【股票代碼】 - 說明:顯示本次分析所針對的股票代號。 2. 【股票價格趨勢與技術指標分析】 - 說明:闡述近期股價變化趨勢、波動情況,並搭配 RSI、MACD、VWAP 等技術指標,提供觀察與解讀。 3. 【技術指標分析與見解】 - 說明:進一步解釋 RSI、MACD 等所呈現的訊號,與可能的後續走勢。 4. 【財務分析】 - 4.1 獲利能力比率分析 - 4.2 流動性比率分析 - 4.3 償債能力比率分析 - 4.4 營運效率比率分析 - 4.5 市場表現比率分析 - 4.6 總結財務健康狀況 5. 【新聞分析】 - 說明:列出近期相關新聞及其對股價、企業形象可能產生的影響。 6. 【產業分析】 - 說明:討論產業趨勢、發展動能與可能風險。 7. 【競爭對手分析】 - 說明:比較主要競爭對手之優勢與劣勢,以及對目標公司的影響。 8. 【最終結論與投資建議】 - 說明:歸納整體分析結果,給出投資人應採取之行動或建議。 9. 【回答使用者問題】 - 針對「Should I buy this stock?」之回應,提出依據上述分析的結論性建議。 ※ 請確保敘述完整、清晰、條理分明、方便一般讀者閱讀。同時避免使用 JSON 格式,改以段落或清單式敘述。 """ def period_to_start_end(period_str: str): """ 根據使用者選擇的時間區間,計算起始與結束日期。 """ now = dt.datetime.now() if period_str == "3mo": start = now - dt.timedelta(weeks=13) elif period_str == "6mo": start = now - dt.timedelta(weeks=26) elif period_str == "1yr": start = now - dt.timedelta(weeks=52) else: # 若未指定或指定其他值,一律視為 3 個月 start = now - dt.timedelta(weeks=13) return start, now def get_stock_prices(ticker: str, period: str = "3mo") -> Union[Dict, str]: """Fetches historical stock price data and technical indicators for a given ticker.""" try: start, end = period_to_start_end(period) print(f"[get_stock_prices] Downloading data for ticker={ticker}, period={period}") print(f"[get_stock_prices] Start={start}, End={end}") data = yf.download( ticker, start=start, end=end, interval='1d' ) print(f"[get_stock_prices] Fetched data shape={data.shape}") if not data.empty: print(f"[get_stock_prices] Head of data:\n{data.head()}") else: print("[get_stock_prices] No data returned from yfinance.") if data.empty: return {"error": f"No stock data found for {ticker}"} # 如果是多層欄位,就平坦化 if data.columns.nlevels > 1: data.columns = [col[0] for col in data.columns] # 設定日期為索引 (DatetimeIndex) data.index = pd.to_datetime(data.index) # 保留一份副本來輸出給 LLM 用 df_for_output = data.copy().reset_index() df_for_output["Date"] = df_for_output["Date"].astype(str) # 另存一份給技術指標用 (確保是 DatetimeIndex) df = data.copy() # 指標只取最後 12 筆即可 n = min(12, len(df)) indicators = {} # RSI rsi_series = RSIIndicator(df['Close'], window=14).rsi().iloc[-n:] indicators["RSI"] = { str(date.date()): float(value) for date, value in rsi_series.dropna().items() } # Stochastic sto_series = StochasticOscillator( df['High'], df['Low'], df['Close'], window=14 ).stoch().iloc[-n:] indicators["Stochastic_Oscillator"] = { str(date.date()): float(value) for date, value in sto_series.dropna().items() } # MACD macd = MACD(df['Close']) macd_series = macd.macd().iloc[-n:] indicators["MACD"] = { str(date.date()): float(value) for date, value in macd_series.dropna().items() } macd_signal_series = macd.macd_signal().iloc[-n:] indicators["MACD_Signal"] = { str(date.date()): float(value) for date, value in macd_signal_series.dropna().items() } # VWAP vwap_series = volume_weighted_average_price( high=df['High'], low=df['Low'], close=df['Close'], volume=df['Volume'] ).iloc[-n:] indicators["vwap"] = { str(date.date()): float(value) for date, value in vwap_series.dropna().items() } return { "stock_price": df_for_output.to_dict(orient="records"), "indicators": indicators } except Exception as e: print(f"[get_stock_prices] Exception: {str(e)}") return f"Error fetching price data: {str(e)}" def get_financial_news(ticker: str) -> Union[Dict, str]: """Fetches the latest financial news related to a given ticker.""" try: print(f"[get_financial_news] Fetching news for ticker={ticker}") stock = yf.Ticker(ticker) news = stock.news # 從 Yahoo Finance 獲取新聞 if not news: print("[get_financial_news] No news data returned from yfinance.") return {"news": "No recent news found."} # 詳細列印前 5 則新聞資訊 latest_news = [] for i, item in enumerate(news[:5], start=1): print(f"[get_financial_news] News item {i} RAW: {json.dumps(item, indent=2)}") latest_news.append({ "title": item.get("title"), "publisher": item.get("publisher"), "link": item.get("link"), "published_date": item.get("providerPublishTime") }) print(f"[get_financial_news] Found {len(latest_news)} news items (detailed above).") return {"news": latest_news} except Exception as e: print(f"[get_financial_news] Exception: {str(e)}") return f"Error fetching news: {str(e)}" def get_financial_metrics(ticker: str) -> Union[Dict, str]: """Fetches key financial ratios for a given ticker.""" try: print(f"[get_financial_metrics] Fetching financial metrics for ticker={ticker}") stock = yf.Ticker(ticker) info = stock.info print(f"[get_financial_metrics] Info returned:\n{json.dumps(info, indent=2)}") return { 'pe_ratio': info.get('forwardPE'), 'price_to_book': info.get('priceToBook'), 'debt_to_equity': info.get('debtToEquity'), 'profit_margins': info.get('profitMargins') } except Exception as e: print(f"[get_financial_metrics] Exception: {str(e)}") return f"Error fetching ratios: {str(e)}" def analyze_stock(api_key: str, ticker: str, period: str) -> str: """ 根據輸入的 LLM API key、股票代號與時間區間,抓取各項資料後, 組合成分析 Prompt 呼叫 LLM,最後回傳『易讀文字段落』的基本面分析結果。 """ try: # 建立 LLM 實例 llm = ChatOpenAI( model='gpt-4o', openai_api_key=api_key, temperature=0 ) # 取得資料 price_data = get_stock_prices(ticker, period) metrics = get_financial_metrics(ticker) news = get_financial_news(ticker) # 組合 Prompt prompt = FUNDAMENTAL_ANALYST_PROMPT.replace("{company}", ticker) user_question = "Should I buy this stock?" analysis_prompt = f""" 根據以下 {ticker} 的資料,進行全面的基本面分析並回答使用者問題:"{user_question}" 股價與技術指標資料: {json.dumps(price_data, ensure_ascii=False, indent=2)} 財務指標: {json.dumps(metrics, ensure_ascii=False, indent=2)} 相關新聞: {json.dumps(news, ensure_ascii=False, indent=2)} 請注意:即使新聞的 description 或 summary 為空,也請務必根據新聞的 title、publisher 或其他字段,推斷其可能對股價與公司形象造成的影響。請將這些新聞具體列在新聞分析章節。 {prompt} """ print("[analyze_stock] Sending prompt to LLM...") response = llm.invoke(analysis_prompt) return response.content # 回傳「非 JSON」的易讀文字段落 except Exception as e: print(f"[analyze_stock] Exception: {str(e)}") return f"分析過程中發生錯誤: {str(e)}\n{traceback.format_exc()}" # --- Gradio 介面 --- iface = gr.Interface( fn=analyze_stock, inputs=[ gr.Textbox(label="LLM API Key", type="password", placeholder="請輸入 OpenAI API Key"), gr.Textbox(label="股票代號", placeholder="例如:TSLA 或 2330.TW"), gr.Dropdown(choices=["3mo", "6mo", "1yr"], label="時間區間", value="3mo") ], outputs=gr.Textbox(label="基本面財務新聞方面分析結果"), title="我應該買這支股票嗎? by DAVID888", description="輸入您的 LLM openai key、股票代號與時間區間,取得該股票的分析報告" ) if __name__ == "__main__": iface.launch()