ceodkwk/datacenterStock
0
1"""2NASDAQ Data Center Stock RAG System - Gradio App3Deploy on Hugging Face Spaces: ceodkwk/datacenterStock4 5HF Spaces entry point - demo.launch() called at module level.6Set HF_TOKEN as a repository secret in Spaces settings.7"""8 9import json10import os11 12import gradio as gr13import plotly.graph_objects as go14import pandas as pd15from datetime import datetime, timedelta16from dotenv import load_dotenv17 18load_dotenv()19 20# ── Import backend ──21from rag_system import RAGPipeline22from crawler import crawl_all, TICKERS as ALL_TICKERS23 24# ── Initialize RAG pipeline (loads index if available) ──25pipeline = RAGPipeline()26pipeline._ensure_loaded()27 28STOCKS_DIR = os.path.join(os.path.dirname(__file__), "data", "stocks")29DATA_DIR = os.path.join(os.path.dirname(__file__), "data")30 31PERIOD_DAYS = {"1개월": 30, "3개월": 90, "6개월": 180, "1년": 365, "2년": 730}32 33 34# ── Helper functions ──35 36def get_ticker_choices():37 tickers = pipeline.get_available_tickers()38 return ["전체 종목"] + tickers39 40 41def load_stock_json(ticker: str) -> dict:42 path = os.path.join(STOCKS_DIR, f"{ticker}.json")43 if not os.path.exists(path):44 return {}45 with open(path, encoding="utf-8") as f:46 return json.load(f)47 48 49def fmt_mc(val):50 if val is None:51 return "N/A"52 if val >= 1e12:53 return f"${val/1e12:.2f}T"54 if val >= 1e9:55 return f"${val/1e9:.2f}B"56 if val >= 1e6:57 return f"${val/1e6:.1f}M"58 return f"${val:,.0f}"59 60 61def fmt_pct(val):62 if val is None:63 return "N/A"64 return f"{val*100:+.1f}%"65 66 67# ── Tab 1: Chat ──68 69def chat_response(message: str, history: list, ticker_filter: str):70 if not message.strip():71 return "", history72 73 result = pipeline.query(message, ticker_filter=ticker_filter if ticker_filter != "전체 종목" else None)74 answer = result["answer"]75 76 sources = result.get("sources", [])77 if sources:78 src_text = "\n\n📎 참고 데이터: " + ", ".join(79 f"{s['ticker']}({s['type']})" for s in sources[:4]80 )81 answer += src_text82 83 history = history or []84 history.append({"role": "user", "content": message})85 history.append({"role": "assistant", "content": answer})86 return "", history87 88 89# ── Tab 1: Chart ──90 91def render_chart(ticker: str, period_label: str) -> go.Figure:92 if not ticker or ticker == "전체 종목":93 fig = go.Figure()94 fig.add_annotation(text="종목을 선택하세요", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=18))95 fig.update_layout(template="plotly_dark", height=420)96 return fig97 98 data = load_stock_json(ticker)99 if not data or not data.get("price_history"):100 fig = go.Figure()101 fig.add_annotation(text=f"{ticker} 데이터 없음\n먼저 데이터를 수집하세요", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=16))102 fig.update_layout(template="plotly_dark", height=420)103 return fig104 105 days = PERIOD_DAYS.get(period_label, 365)106 cutoff = datetime.now() - timedelta(days=days)107 history = [r for r in data["price_history"] if datetime.strptime(r["date"], "%Y-%m-%d") >= cutoff]108 109 if not history:110 history = data["price_history"]111 112 dates = [r["date"] for r in history]113 opens = [r["open"] for r in history]114 highs = [r["high"] for r in history]115 lows = [r["low"] for r in history]116 closes = [r["close"] for r in history]117 volumes = [r["volume"] or 0 for r in history]118 119 # Compute SMAs120 closes_series = pd.Series(closes, index=dates)121 sma20 = closes_series.rolling(20).mean()122 sma50 = closes_series.rolling(50).mean()123 124 fig = go.Figure()125 126 # Candlestick127 fig.add_trace(go.Candlestick(128 x=dates, open=opens, high=highs, low=lows, close=closes,129 name=ticker,130 increasing_line_color="#26a69a",131 decreasing_line_color="#ef5350",132 ))133 134 # SMA lines135 fig.add_trace(go.Scatter(x=dates, y=sma20.tolist(), name="SMA20", line=dict(color="#FFA726", width=1.2), opacity=0.8))136 fig.add_trace(go.Scatter(x=dates, y=sma50.tolist(), name="SMA50", line=dict(color="#42A5F5", width=1.2), opacity=0.8))137 138 # Volume (secondary y-axis)139 fig.add_trace(go.Bar(x=dates, y=volumes, name="Volume", marker_color="rgba(120,120,200,0.3)", yaxis="y2"))140 141 tech = data.get("technical_indicators", {})142 current_price = tech.get("current_price") or (closes[-1] if closes else None)143 change_1y = tech.get("price_change_1y")144 trend = tech.get("trend", "")145 146 title = f"{ticker} — {data.get('name', ticker)}"147 if current_price:148 title += f" | ${current_price:.2f}"149 if change_1y is not None:150 title += f" | 1Y: {change_1y*100:+.1f}%"151 if trend:152 title += f" | Trend: {trend.upper()}"153 154 fig.update_layout(155 title=title,156 template="plotly_dark",157 height=420,158 xaxis_rangeslider_visible=False,159 yaxis=dict(title="Price (USD)", side="left"),160 yaxis2=dict(title="Volume", overlaying="y", side="right", showgrid=False),161 legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),162 margin=dict(l=20, r=20, t=60, b=20),163 )164 return fig165 166 167# ── Tab 2: Screener ──168 169def render_screener(selected_tickers: list) -> pd.DataFrame:170 if not selected_tickers:171 selected_tickers = pipeline.get_available_tickers()172 173 rows = []174 for t in selected_tickers:175 data = load_stock_json(t)176 if not data:177 continue178 info = data.get("info", {})179 tech = data.get("technical_indicators", {})180 rows.append({181 "Ticker": t,182 "Name": data.get("name", t)[:25],183 "Price": f"${tech.get('current_price') or info.get('currentPrice') or 0:.2f}",184 "Market Cap": fmt_mc(info.get("marketCap")),185 "P/E (TTM)": f"{info.get('trailingPE'):.1f}" if info.get("trailingPE") else "N/A",186 "Revenue (TTM)": fmt_mc(info.get("totalRevenue")),187 "FCF": fmt_mc(info.get("freeCashflow")),188 "1Y Return": fmt_pct(tech.get("price_change_1y")),189 "3M Return": fmt_pct(tech.get("price_change_3m")),190 "RSI(14)": f"{tech.get('rsi_14'):.1f}" if tech.get("rsi_14") else "N/A",191 "Trend": (tech.get("trend") or "N/A").upper(),192 })193 194 if not rows:195 return pd.DataFrame()196 197 df = pd.DataFrame(rows)198 return df199 200 201# ── Tab 3: Data Status & Refresh ──202 203def get_data_status() -> pd.DataFrame:204 ts_path = os.path.join(DATA_DIR, "last_updated.json")205 if not os.path.exists(ts_path):206 return pd.DataFrame({"상태": ["데이터 없음. '데이터 수집 시작' 버튼을 눌러주세요."]})207 208 with open(ts_path, encoding="utf-8") as f:209 timestamps = json.load(f)210 211 rows = []212 for ticker, ts in sorted(timestamps.items()):213 if "error" in str(ts):214 rows.append({"Ticker": ticker, "Last Updated": "ERROR", "Status": ts})215 else:216 rows.append({"Ticker": ticker, "Last Updated": ts[:19].replace("T", " "), "Status": "OK"})217 218 return pd.DataFrame(rows)219 220 221def refresh_data(progress=gr.Progress()):222 """Crawl all tickers and rebuild index."""223 progress(0, desc="데이터 수집 시작...")224 225 progress(0.1, desc="yfinance에서 주가/재무 데이터 수집 중 (약 1-2분 소요)...")226 try:227 crawl_all()228 except Exception as e:229 return f"크롤링 오류: {e}"230 231 progress(0.8, desc="RAG 인덱스 재빌드 중...")232 try:233 msg = pipeline.rebuild_index()234 except Exception as e:235 return f"인덱스 빌드 오류: {e}"236 237 progress(1.0, desc="완료!")238 return f"데이터 수집 및 인덱스 재빌드 완료!\n{msg}\n시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"239 240 241# ── Build Gradio App ──242 243ticker_choices = get_ticker_choices()244available_tickers = [t for t in ticker_choices if t != "전체 종목"]245 246with gr.Blocks(247 theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"),248 title="NASDAQ Data Center Stock AI Analyst",249 css=".gradio-container {max-width: 1400px !important}",250) as demo:251 252 gr.Markdown(253 """254# 📊 NASDAQ 데이터센터 주식 AI 애널리스트255**대상 종목**: CIFR, IREN, APLD, MARA, RIOT, CORZ, HUT, WULF, CLSK, BTBT | NVDA, AMD, SMCI, ANET, DELL | EQIX, DLR, CLS, VRT256 257최근 2년치 주가·재무 데이터 기반 RAG 시스템 | Powered by Mistral-7B + FAISS258 """259 )260 261 with gr.Tabs():262 263 # ── Tab 1: AI Chat ──────────────────────────────────────────────────────264 with gr.Tab("💬 AI 애널리스트 채팅"):265 with gr.Row():266 with gr.Column(scale=5):267 ticker_filter = gr.Dropdown(268 choices=ticker_choices,269 value="전체 종목",270 label="종목 필터 (선택 시 해당 종목 데이터 우선 검색)",271 interactive=True,272 )273 chatbot = gr.Chatbot(274 label="AI 애널리스트",275 height=480,276 type="messages",277 show_copy_button=True,278 )279 with gr.Row():280 msg_input = gr.Textbox(281 placeholder="예: CIFR의 최근 1년 주가 성과는? / 데이터센터 REIT 중 가장 재무가 좋은 종목은?",282 label="질문 입력",283 scale=8,284 lines=2,285 )286 with gr.Column(scale=2, min_width=120):287 submit_btn = gr.Button("전송", variant="primary")288 clear_btn = gr.Button("초기화")289 290 gr.Examples(291 examples=[292 ["CIFR와 IREN 중 어느 종목이 기술적으로 더 강한가?"],293 ["비트코인 채굴 관련주 중 시총이 가장 큰 종목은?"],294 ["NVDA의 최근 1년 주가 수익률과 트렌드를 분석해줘"],295 ["EQIX와 DLR 데이터센터 REIT 재무 비교해줘"],296 ["RSI가 과매도 구간인 종목이 있나요?"],297 ["향후 주가 상승 가능성이 높은 AI 인프라 종목을 추천해줘"],298 ],299 inputs=msg_input,300 )301 302 with gr.Column(scale=5):303 chart_ticker = gr.Dropdown(304 choices=available_tickers or ["CIFR"],305 value=available_tickers[0] if available_tickers else "CIFR",306 label="차트 종목 선택",307 interactive=True,308 )309 chart_period = gr.Radio(310 choices=["1개월", "3개월", "6개월", "1년", "2년"],311 value="1년",312 label="기간",313 interactive=True,314 )315 chart_plot = gr.Plot(label="주가 차트 (캔들스틱 + SMA20/50 + 거래량)")316 317 # Events318 submit_btn.click(chat_response, [msg_input, chatbot, ticker_filter], [msg_input, chatbot])319 msg_input.submit(chat_response, [msg_input, chatbot, ticker_filter], [msg_input, chatbot])320 clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_input])321 chart_ticker.change(render_chart, [chart_ticker, chart_period], chart_plot)322 chart_period.change(render_chart, [chart_ticker, chart_period], chart_plot)323 324 # ── Tab 2: Screener ─────────────────────────────────────────────────────325 with gr.Tab("📋 종목 스크리너"):326 gr.Markdown("### 종목 비교 테이블")327 screener_select = gr.Dropdown(328 choices=available_tickers,329 value=available_tickers[:10] if len(available_tickers) >= 10 else available_tickers,330 label="비교할 종목 선택 (다중 선택 가능)",331 multiselect=True,332 interactive=True,333 )334 screener_btn = gr.Button("테이블 업데이트", variant="secondary")335 screener_table = gr.DataFrame(336 label="종목 비교",337 interactive=False,338 )339 screener_btn.click(render_screener, [screener_select], screener_table)340 341 # ── Tab 3: Data Management ──────────────────────────────────────────────342 with gr.Tab("🔄 데이터 관리"):343 gr.Markdown(344 """345### 데이터 수집 & 인덱스 관리346 347- **데이터 수집**: yfinance로 19개 종목의 최근 2년 주가/재무 데이터를 수집합니다 (약 1~2분 소요).348- **인덱스 재빌드**: 수집된 데이터를 임베딩하여 RAG 검색 인덱스를 생성합니다.349- HF Spaces 배포 시: 데이터가 이미 포함된 경우 수집 불필요. 업데이트 시 버튼 클릭.350 """351 )352 with gr.Row():353 refresh_btn = gr.Button("📥 데이터 수집 시작 (크롤링 + 인덱스 재빌드)", variant="primary", scale=2)354 status_btn = gr.Button("📋 현재 데이터 상태 확인", variant="secondary", scale=1)355 356 refresh_output = gr.Textbox(label="진행 상황", lines=4, interactive=False)357 status_table = gr.DataFrame(label="종목별 마지막 업데이트", interactive=False)358 359 refresh_btn.click(refresh_data, outputs=refresh_output)360 status_btn.click(get_data_status, outputs=status_table)361 362 # Load initial chart on startup363 demo.load(364 fn=lambda: render_chart(available_tickers[0] if available_tickers else "", "1년"),365 outputs=chart_plot,366 )367 demo.load(fn=get_data_status, outputs=status_table)368 369 370# HF Spaces entry point (no if __name__ guard)371demo.launch()372 