File size: 12,108 Bytes
0dbb99d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4593632
0dbb99d
 
 
fb2652f
0dbb99d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76e906f
 
 
 
 
0dbb99d
 
 
 
 
 
 
 
fb2652f
 
76e906f
fb2652f
 
76e906f
fb2652f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0dbb99d
fb2652f
 
 
 
 
 
 
 
 
 
 
 
 
 
4593632
0dbb99d
 
 
 
 
 
 
 
 
 
 
 
76e906f
0dbb99d
 
 
cdf1418
76e906f
0dbb99d
cdf1418
0255349
 
 
0dbb99d
 
cdf1418
 
0dbb99d
 
2154d91
0255349
 
 
 
 
 
2154d91
ee48331
 
4593632
ee48331
 
2154d91
4593632
 
2154d91
4593632
 
 
 
 
2154d91
 
76e906f
2154d91
ee48331
76e906f
 
ee48331
2154d91
 
4593632
 
2154d91
0dbb99d
ee48331
4593632
 
 
 
 
 
2154d91
4593632
 
2154d91
0dbb99d
ee48331
0dbb99d
2154d91
 
4593632
 
2154d91
 
 
4593632
 
2154d91
0dbb99d
ee48331
0dbb99d
 
 
 
 
2154d91
 
4593632
 
2154d91
 
 
4593632
ee48331
2154d91
0dbb99d
0255349
0dbb99d
 
 
ee48331
0dbb99d
0255349
0dbb99d
ee48331
0255349
0dbb99d
0255349
0dbb99d
ee48331
fb2652f
ee48331
fb2652f
 
ee48331
fb2652f
 
 
 
ee48331
0255349
fb2652f
0dbb99d
 
0255349
0dbb99d
 
 
ee48331
0dbb99d
0255349
0dbb99d
 
4593632
0255349
 
0dbb99d
 
 
 
 
 
 
0255349
0dbb99d
 
 
 
 
fb2652f
0dbb99d
 
 
 
 
 
 
 
76e906f
0dbb99d
 
 
 
 
76e906f
0dbb99d
 
7236baa
 
0dbb99d
 
 
 
 
 
 
 
 
 
 
 
7236baa
0dbb99d
 
76e906f
0255349
0dbb99d
0255349
fb2652f
0dbb99d
0255349
0dbb99d
 
 
 
 
 
 
 
 
 
6953dde
 
 
0dbb99d
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
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()