Stock-Analyzer / app.py
tbdavid2019's picture
標題更改
6953dde verified
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()