CoolFace
Apppublic

ceodkwk/datacenterStock

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
app.py372 linesDownload Raw Back to root
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