Spaces:
Running
Running
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() |