Spaces:
Sleeping
Sleeping
File size: 10,595 Bytes
865ea69 36f18eb 97f0d4d 36f18eb 257d0e4 60e84ae 39b3613 c56cd11 865ea69 97f0d4d e3a9bb8 9c7f8b0 97f0d4d 36f18eb 1ed4a3d 915b378 97f0d4d 36f18eb 97f0d4d a79596f 60e84ae 36f18eb 60e84ae a79596f 97f0d4d 1ed4a3d 97f0d4d 36f18eb 770a028 36f18eb 1ed4a3d 36f18eb 1ed4a3d 36f18eb 97f0d4d 36f18eb 97f0d4d 36f18eb 97f0d4d 60e84ae 38c66d4 60e84ae a79596f c363444 60e84ae c363444 a79596f c363444 a79596f 6d50111 38c66d4 a79596f 38c66d4 a79596f c363444 a79596f 6d50111 38c66d4 c363444 6d50111 c363444 a79596f c363444 865ea69 c4352db 36f18eb 865ea69 36f18eb ef10992 c4352db 1ed4a3d 97f0d4d 36f18eb 97f0d4d 1ed4a3d 97f0d4d b1a327c 8cace85 97f0d4d a8649f3 824ab65 36f18eb 1ed4a3d a8649f3 824ab65 a8649f3 824ab65 35f815b ef10992 a8649f3 97f0d4d 770a028 60e84ae 865ea69 257d0e4 36f18eb 865ea69 770a028 97f0d4d 36f18eb a8649f3 770a028 865ea69 257d0e4 770a028 60e84ae 770a028 865ea69 97f0d4d 770a028 c4352db 97f0d4d 865ea69 97f0d4d 36f18eb 97f0d4d 39b3613 e3a9bb8 88973b1 36f18eb 770a028 865ea69 770a028 | 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 | import os
import datetime
import requests
from flask import Flask, render_template, request
import openai
import yfinance as yf
import plotly.graph_objs as go
app = Flask(__name__)
FINNHUB_API_KEY = os.getenv("FINNHUB_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
industry_mapping = {
"Technology": "科技業",
"Financial Services": "金融服務業",
"Healthcare": "醫療保健業",
"Consumer Cyclical": "非必需消費品業",
"Communication Services": "通訊服務業",
"Energy": "能源業",
"Industrials": "工業類股",
"Utilities": "公用事業",
"Real Estate": "房地產業",
"Materials": "原物料業",
"Consumer Defensive": "必需消費品業",
"Unknown": "未知"
}
IMPORTANT_METRICS = [
"peTTM", "pb", "roeTTM", "roaTTM", "grossMarginTTM",
"revenueGrowthTTMYoy", "epsGrowthTTMYoy", "debtToEquityAnnual"
]
METRIC_NAMES_ZH_EN = {
"peTTM": "本益比 (PE TTM)",
"pb": "股價淨值比 (PB)",
"roeTTM": "股東權益報酬率 (ROE TTM)",
"roaTTM": "資產報酬率 (ROA TTM)",
"grossMarginTTM": "毛利率 (Gross Margin TTM)",
"revenueGrowthTTMYoy": "營收成長率 (YoY)",
"epsGrowthTTMYoy": "每股盈餘成長率 (EPS Growth YoY)",
"debtToEquityAnnual": "負債權益比 (Debt to Equity Annual)"
}
QUOTE_FIELDS = {
"c": ("即時股價", "Current Price"),
"o": ("開盤價", "Open"),
"h": ("最高價", "High"),
"l": ("最低價", "Low"),
"pc": ("前收盤價", "Previous Close"),
"dp": ("漲跌幅(%)", "Change Percent")
}
def get_finnhub_json(endpoint, params):
url = f"https://finnhub.io/api/v1/{endpoint}"
params["token"] = FINNHUB_API_KEY
try:
r = requests.get(url, params=params, timeout=5)
r.raise_for_status()
return r.json()
except Exception as e:
print(f"Request error {endpoint} with params {params}: {e}")
return {}
def get_quote(symbol):
data = get_finnhub_json("quote", {"symbol": symbol})
return {k: (round(data.get(k), 4) if isinstance(data.get(k), (int, float)) else data.get(k)) for k in QUOTE_FIELDS.keys()}
def get_metrics(symbol):
data = get_finnhub_json("stock/metric", {"symbol": symbol, "metric": "all"})
return data.get("metric", {})
def filter_metrics(metrics):
filtered = {}
for key in IMPORTANT_METRICS:
v = metrics.get(key)
if v is not None:
if any(x in key.lower() for x in ["growth", "margin", "roe", "roa"]):
try:
filtered[key] = f"{float(v):.2f}%"
except:
filtered[key] = str(v)
else:
try:
filtered[key] = round(float(v), 4)
except:
filtered[key] = str(v)
return filtered
def get_recent_news(symbol):
today = datetime.datetime.now().strftime("%Y-%m-%d")
past = (datetime.datetime.now() - datetime.timedelta(days=10)).strftime("%Y-%m-%d")
news = get_finnhub_json("company-news", {"symbol": symbol, "from": past, "to": today})
if not isinstance(news, list):
return []
news = sorted(news, key=lambda x: x.get("datetime", 0), reverse=True)[:10]
for n in news:
try:
n["datetime"] = datetime.datetime.utcfromtimestamp(n["datetime"]).strftime("%Y-%m-%d %H:%M")
except:
n["datetime"] = "未知時間"
return news
def get_company_profile(symbol):
return get_finnhub_json("stock/profile2", {"symbol": symbol})
def adjust_symbol_for_yfinance(symbol):
""" 如果是台灣股票,若沒帶 .TW 則補上 """
if symbol.isalpha() and len(symbol) <= 5: # 簡單判斷台股代碼長度
if not symbol.endswith(".TW"):
symbol += ".TW"
return symbol
def get_7day_stock_plot(symbol):
try:
yf_symbol = adjust_symbol_for_yfinance(symbol)
end_date = datetime.datetime.today()
start_date = end_date - datetime.timedelta(days=14) # account for weekends
df = yf.download(yf_symbol, start=start_date.strftime('%Y-%m-%d'), end=end_date.strftime('%Y-%m-%d'), progress=False)
if df.empty or 'Close' not in df.columns:
return "<p class='text-danger'>📊 無法取得股價趨勢圖。</p>"
df = df.tail(7) # recent 7 trading days
dates = df.index.strftime('%Y-%m-%d').tolist()
closes = df['Close'].round(2).tolist()
fig = go.Figure()
fig.add_trace(go.Scatter(x=dates, y=closes, mode='lines+markers', name='Close Price'))
fig.update_layout(
title=f"📉 {symbol} 最近7日收盤價走勢 / 7-Day Closing Price Trend",
xaxis_title="日期 / Date",
yaxis_title="收盤價 (USD)",
template="plotly_white",
height=400
)
return fig.to_html(full_html=False, include_plotlyjs='cdn', default_height="400px", default_width="100%")
except Exception as e:
print(f"[Plot Error] Failed to generate 7-day stock plot for {symbol}: {e}")
return "<p class='text-danger'>📊 無法取得股價趨勢圖。</p>"
@app.route("/", methods=["GET", "POST"])
def index():
result = {}
symbol = ""
if request.method == "POST":
symbol = request.form.get("symbol", "").strip().upper()
if not symbol:
result["error"] = "請輸入股票代號 / Please enter a stock symbol"
return render_template("index.html", result=result, symbol_input=symbol)
try:
quote = get_quote(symbol)
metrics = get_metrics(symbol)
filtered_metrics = filter_metrics(metrics)
news = get_recent_news(symbol)
profile = get_company_profile(symbol)
industry_en = profile.get("finnhubIndustry", "Unknown")
industry_zh = industry_mapping.get(industry_en, "未知")
plot_html = get_7day_stock_plot(symbol)
if filtered_metrics:
prompt = f"""
請根據以下資訊,產出一份專業、詳細且內容完全對應的中英文雙語股票技術分析與投資建議報告,請將中文與英文分段清楚列出,內容一致且詳盡,並使用分點說明:
股票代號: {symbol}
目前價格: {quote.get('c', 'N/A')} 美元 (USD)
產業分類: {industry_zh} ({industry_en})
重要財務指標: {filtered_metrics}
請內容涵蓋:
1. 價格趨勢與技術指標分析(如RSI, 移動平均線等)
2. 重要財務數據對股價的支持或風險提示
3. 明確且具體的買入、目標價、止損和加碼或減碼策略
4. 結論與投資建議
---
Please generate a professional and detailed bilingual (Chinese and English) technical analysis and investment recommendation report based on the above data.
Make sure the Chinese and English parts are clearly separated and correspond exactly in content and detail. Use numbered points matching the Chinese section:
1. Price trend and technical indicators analysis (e.g., RSI, moving averages)
2. Key financial metrics supporting or warning risks for the stock price
3. Clear and specific buy, target price, stop loss, and position adjustment strategies
4. Conclusion and investment advice
---
請務必完整且清楚地列出中英文報告,避免內容不對稱或省略。
"""
else:
prompt = f"""
由於缺乏完整的歷史價格與財務資料,請針對股票代號 {symbol},以專業且詳細的語氣產出一份中英文雙語且內容一致的股票技術分析與投資建議報告,請將中英文分段清楚列出,內容完整且分點說明,包含趨勢分析、支撐阻力、成交量、技術指標、風險提示與具體操作建議。
---
Because of incomplete historical price and financial data, please generate a professional, detailed, and content-matched bilingual (Chinese and English) technical analysis and investment recommendation report for the stock symbol {symbol}.
Make sure the Chinese and English parts are clearly separated and have exactly the same content, including numbered points covering trend analysis, support and resistance, volume, technical indicators, risk warnings, and specific investment strategies.
---
請務必用清晰的格式和專業語氣回覆,確保中英文內容對等。
"""
# OpenAI ChatCompletion 呼叫(根據你使用的 openai 版本調整)
try:
chat_response = openai.ChatCompletion.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"你是一位中英雙語的金融分析助理,"
"請同時用中文和英文回覆,且中英文內容需完全對等且分段清楚。"
)
},
{"role": "user", "content": prompt}
],
max_tokens=900,
temperature=0.6,
)
gpt_analysis = chat_response['choices'][0]['message']['content'].strip()
# 判斷是否為預設問候語,若是則回報錯誤
if "您好" in gpt_analysis and ("帮助您" in gpt_analysis or "帮助" in gpt_analysis):
gpt_analysis = "❌ GPT 回應似乎是預設問候語,可能 prompt 未正確生效或 API 請求有誤。"
else:
if "技術分析" in gpt_analysis:
gpt_analysis += "\n\n---\n\n*以上分析僅供參考,投資有風險,入市需謹慎。*"
except Exception as e:
gpt_analysis = f"❌ GPT 回應錯誤: {e}"
result = {
"symbol": symbol,
"quote": quote,
"industry_en": industry_en,
"industry_zh": industry_zh,
"metrics": filtered_metrics,
"news": news,
"gpt_analysis": gpt_analysis,
"plot_html": plot_html
}
except Exception as e:
result = {"error": f"資料讀取錯誤: {e}"}
return render_template(
"index.html",
result=result,
symbol_input=symbol,
QUOTE_FIELDS=QUOTE_FIELDS,
METRIC_NAMES_ZH_EN=METRIC_NAMES_ZH_EN
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860, debug=True)
|