diff --git a/project/STOCK_APP/.idea/.gitignore b/project/STOCK_APP/.idea/.gitignore new file mode 100644 index 0000000..93bca08 --- /dev/null +++ b/project/STOCK_APP/.idea/.gitignore @@ -0,0 +1,10 @@ +# 디폴트 무시된 파일 +/shelf/ +/workspace.xml +# 에디터 기반 HTTP 클라이언트 요청 +/httpRequests/ +# 쿼리 파일을 포함한 무시된 디폴트 폴더 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/project/STOCK_APP/.idea/STOCK_APP.iml b/project/STOCK_APP/.idea/STOCK_APP.iml new file mode 100644 index 0000000..bf1af76 --- /dev/null +++ b/project/STOCK_APP/.idea/STOCK_APP.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/project/STOCK_APP/.idea/inspectionProfiles/profiles_settings.xml b/project/STOCK_APP/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/project/STOCK_APP/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/project/STOCK_APP/.idea/modules.xml b/project/STOCK_APP/.idea/modules.xml new file mode 100644 index 0000000..20718f7 --- /dev/null +++ b/project/STOCK_APP/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/project/STOCK_APP/.idea/pyLspTools.xml b/project/STOCK_APP/.idea/pyLspTools.xml new file mode 100644 index 0000000..e202fc5 --- /dev/null +++ b/project/STOCK_APP/.idea/pyLspTools.xml @@ -0,0 +1,34 @@ + + + + + + \ No newline at end of file diff --git a/project/STOCK_APP/.idea/vcs.xml b/project/STOCK_APP/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/project/STOCK_APP/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/project/STOCK_APP/backend/ai/embedding.py b/project/STOCK_APP/backend/ai/embedding.py new file mode 100644 index 0000000..4f1a98a --- /dev/null +++ b/project/STOCK_APP/backend/ai/embedding.py @@ -0,0 +1,9 @@ +from langchain_ibm import WatsonxEmbeddings +from backend.config.settings import settings + +watson_embedding = WatsonxEmbeddings( + model_id="ibm/granite-embedding-278m-multilingual", + url=f"{settings.watsonx_url}", + api_key=f"{settings.watsonx_api_key}", + project_id=f"{settings.watsonx_project_id}", +) \ No newline at end of file diff --git a/project/STOCK_APP/backend/ai/llm.py b/project/STOCK_APP/backend/ai/llm.py new file mode 100644 index 0000000..5f52f60 --- /dev/null +++ b/project/STOCK_APP/backend/ai/llm.py @@ -0,0 +1,17 @@ +from langchain_ibm import ChatWatsonx +from backend.config.settings import settings +from langchain_openai import ChatOpenAI + +watson_llm = ChatWatsonx( + model_id="ibm/granite-4-h-small", + url=f"{settings.watsonx_url}", + api_key=f"{settings.watsonx_api_key}", + project_id=f"{settings.watsonx_project_id}", + max_tokens=2000, +) + +hugging_llm = ChatOpenAI( + base_url="https://router.huggingface.co/v1", + api_key=f"{settings.hf_token}", + model_name="Qwen/Qwen3-8B:nscale", +) \ No newline at end of file diff --git a/project/STOCK_APP/backend/config/settings.py b/project/STOCK_APP/backend/config/settings.py new file mode 100644 index 0000000..1d35237 --- /dev/null +++ b/project/STOCK_APP/backend/config/settings.py @@ -0,0 +1,12 @@ +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file="backend/.env", extra="ignore") + # 사용할 모델 + watsonx_api_key: str = Field(alias="WATSONX_API_KEY") + watsonx_project_id: str = Field(alias="WATSONX_PROJECT_ID") + watsonx_url: str = Field(alias="WATSONX_URL") + hf_token: str = Field(alias="HF_TOKEN") + +settings = Settings() \ No newline at end of file diff --git a/project/STOCK_APP/backend/graph/nodes.py b/project/STOCK_APP/backend/graph/nodes.py new file mode 100644 index 0000000..948df77 --- /dev/null +++ b/project/STOCK_APP/backend/graph/nodes.py @@ -0,0 +1,21 @@ +from backend.services.news_service import get_news +from backend.services.financial_service import get_financial_info +from backend.services.technical_service import get_technical_info + +async def news_node(state): + news_list = await get_news(state["company_name"]) + return {"news": news_list} + +async def financial_node(state): + financials = await get_financial_info(state["ticker"]) + return {"financials": financials} + +async def technical_node(state): + technicals = await get_technical_info(state["ticker"]) + return {"technicals": technicals} + +async def competitor_node(state): + pass + +async def report_node(state): + pass diff --git a/project/STOCK_APP/backend/graph/state.py b/project/STOCK_APP/backend/graph/state.py new file mode 100644 index 0000000..b936935 --- /dev/null +++ b/project/STOCK_APP/backend/graph/state.py @@ -0,0 +1,11 @@ +from typing import TypedDict + +class StockAnalysis(TypedDict): + query:str + ticker:str + company_name:str + new:list + financials:dict + technicals:dict + competitors:list + report:str diff --git a/project/STOCK_APP/backend/main.py b/project/STOCK_APP/backend/main.py new file mode 100644 index 0000000..0be982c --- /dev/null +++ b/project/STOCK_APP/backend/main.py @@ -0,0 +1,50 @@ +from fastapi import FastAPI +from starlette.staticfiles import StaticFiles +from contextlib import asynccontextmanager + +from backend.routers.stock_router import router as stock_router + +# from backend.repository.db_init import Base, SessionLocal, engine +# +# from backend.routers.evalu_router import router as evalu_router +# from backend.routers.assistant_router import router as assistant_router +# from backend.routers.call_router import router as call_router +# from backend.repository.seed import seed_customers + +# @asynccontextmanager +# async def lifespan(app: FastAPI): +# print("서버 시작") +# +# # Base 에 등록된 모든 모델에 테이블 자동 생성 +# Base.metadata.create_all(bind=engine) +# print("[DB] 테이블 생성 완료 (또는 이미 존재)") +# +# db = SessionLocal() +# try: +# # 기본 user 데이터 삽입 +# seed_customers(db) +# finally: +# db.close() +# +# yield +# print("서버 종료") + +# app = FastAPI() +# app = FastAPI(title="상담 LLM", version="1.0", lifespan=lifespan) +app = FastAPI(title="$ Stock AI", version="1.0", description=""" + #### $주식 AI 분석 FastAPI + LangGraph + yfinance + ChromaDB + + ### 주요 기능 + | 번호 | 기능 | 엔드포인트 | + | --- | --- | --- | + |1 | 기업 종합 분석 | 'POST /api/stock/analysis' + """, +) + +# static 폴더 지정 +# app.mount("/static", StaticFiles(directory="backend/static"), name="static") + +# 라우터 등록 +app.include_router(stock_router) +# app.include_router(assistant_router) +# app.include_router(evalu_router) \ No newline at end of file diff --git a/project/STOCK_APP/backend/routers/stock_router.py b/project/STOCK_APP/backend/routers/stock_router.py new file mode 100644 index 0000000..a0eaefc --- /dev/null +++ b/project/STOCK_APP/backend/routers/stock_router.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from backend.schemas.stock_schemas import StockAnalyzeReq +from backend.services.stock_service import StockService + +router = APIRouter(prefix="/api/stock", tags=["기헙종합분석"]) + +@router.post("/analyze", summary="기업 종합 분석", description="자연어로 해당기업을 요청하면 뉴스, 재무, 기술적, 경쟁사 분석을 수행합니다.", ) +async def stock_analyze(req:StockAnalyzeReq): + service = StockService() + return await service._extract(req.query) \ No newline at end of file diff --git a/project/STOCK_APP/backend/schemas/news_schemas.py b/project/STOCK_APP/backend/schemas/news_schemas.py new file mode 100644 index 0000000..574d7ea --- /dev/null +++ b/project/STOCK_APP/backend/schemas/news_schemas.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class NewsItem(BaseModel): + title: str + snippet: str + url: str + source: str + date: str \ No newline at end of file diff --git a/project/STOCK_APP/backend/schemas/stock_schemas.py b/project/STOCK_APP/backend/schemas/stock_schemas.py new file mode 100644 index 0000000..eeae064 --- /dev/null +++ b/project/STOCK_APP/backend/schemas/stock_schemas.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class StockAnalyzeReq(BaseModel): + query: str + +class TickerInfo(BaseModel): + ticker: str + company_name: str \ No newline at end of file diff --git a/project/STOCK_APP/backend/services/financial_service.py b/project/STOCK_APP/backend/services/financial_service.py new file mode 100644 index 0000000..de7ddee --- /dev/null +++ b/project/STOCK_APP/backend/services/financial_service.py @@ -0,0 +1,36 @@ +import yahooquery as yf + +async def get_financial_info(ticker: str): + """ + yfinance api 이용 + """ + company = yf.Ticker(ticker) + + info = company.info + income = company.income_stmtf + balance = company.balance_sheet + cashflow = company.cash_flow + + return { + # 시가 총액 + "market_cap":info.get("marketCap"), + # 현재 주가 + "current_price":info.get("currentPrice"), + # 매출 + "revenue":income.loc["Total Revenue"].iloc[0], + # 손익계산 + "operating_income":income.loc["Operating Income"].iloc[0], + # 순이익 + "net_income":income.loc["Net Income"].iloc[0], + # 총자산 + "total_assets":balance.loc["Total Assets"].iloc[0], + # 총부채 + "total_debt":balance.loc["Total Debt"].iloc[0], + # 현금흐름 + "free_cash_flow":cashflow.loc["Free Cash Flow"].iloc[0], + # per + "pe_ratio":info.get("trailingPE"), + # pbr + "pb_ratio":info.get("priceToBook") + } + diff --git a/project/STOCK_APP/backend/services/news_service.py b/project/STOCK_APP/backend/services/news_service.py new file mode 100644 index 0000000..a90b99a --- /dev/null +++ b/project/STOCK_APP/backend/services/news_service.py @@ -0,0 +1,26 @@ +from langchain_community.utilities import GoogleSerperAPIWrapper + +from backend.schemas.news_schemas import NewsItem + + +async def get_news(company_name: str): + """ + 구글 뉴스 검색 후 + title, snippet, url, source, date 추출 + """ + search = GoogleSerperAPIWrapper(type="news") + results = await search.results(f"{company_name} stock news") + news_list = [] + + for item in results['news'][:10]: + news_list.append( + NewsItem( + title=item.get("title", ""), + snippet=item.get("snippet", ""), + url=item.get("link", ""), + source=item.get("source", ""), + date=item.get("date", "") + ) + ) + + return news_list \ No newline at end of file diff --git a/project/STOCK_APP/backend/services/stock_service.py b/project/STOCK_APP/backend/services/stock_service.py new file mode 100644 index 0000000..4f0b8ff --- /dev/null +++ b/project/STOCK_APP/backend/services/stock_service.py @@ -0,0 +1,148 @@ +from backend.schemas.stock_schemas import TickerInfo +import yahooquery as yq +from fastapi import HTTPException + +COMPANY_MAP = { + "NVDA": { + "company_name": "NVIDIA", + "aliases": ["엔비디아", "nvidia", "nvda"], + }, + "AAPL": { + "company_name": "Apple", + "aliases": ["애플", "apple", "aapl"], + }, + "MSFT": { + "company_name": "Microsoft", + "aliases": ["마이크로소프트", "microsoft", "msft"], + }, + "TSLA": { + "company_name": "Tesla", + "aliases": ["테슬라", "tesla", "tsla"], + }, + "GOOGL": { + "company_name": "Alphabet (Google)", + "aliases": ["구글", "google", "알파벳", "alphabet", "googl"], + }, + "AMZN": { + "company_name": "Amazon", + "aliases": ["아마존", "amazon", "amzn"], + }, + "META": { + "company_name": "Meta Platforms", + "aliases": ["메타", "meta", "페이스북", "facebook", "fb"], + }, + "AMD": { + "company_name": "Advanced Micro Devices", + "aliases": ["amd"], + }, + "INTC": { + "company_name": "Intel", + "aliases": ["인텔", "intel", "intc"], + }, + "TSM": { + "company_name": "TSMC", + "aliases": ["tsmc", "tsm", "대만반도체"], + }, + "QCOM": { + "company_name": "Qualcomm", + "aliases": ["퀄컴", "qualcomm", "qcom"], + }, + "AVGO": { + "company_name": "Broadcom", + "aliases": ["브로드컴", "broadcom", "avgo"], + }, + "MU": { + "company_name": "Micron Technology", + "aliases": ["마이크론", "micron", "mu"], + }, + "V": { + "company_name": "Visa", + "aliases": ["비자", "visa", "v"], + }, + "MA": { + "company_name": "Mastercard", + "aliases": ["마스터카드", "mastercard", "ma"], + }, + "JPM": { + "company_name": "JPMorgan Chase", + "aliases": ["jp모건", "jpmorgan", "jpm"], + }, + "JNJ": { + "company_name": "Johnson & Johnson", + "aliases": ["존슨앤존슨", "johnson", "jnj"], + }, + "MSFT": { + "company_name": "Microsoft", + "aliases": ["마이크로소프트", "microsoft", "msft"], + }, +} + +class StockService: + """주식 분석""" + async def _extract(self, query: str): + """ + 사용자 쿼리에서 티커와 회사명 추출 + 1차 : Map 이용 + 2차 : yahooquery 이용 + + query : 엔비디아 분석해줘 + """ + keyword = self._extract_company_keyword(query) + + # 1 차 Map + result = self._extract_ticker_from_query(keyword) + + if result: + return result + + # 2 차 yahooquery + result = await self._search_yahoo_symbol(keyword) + + if result: + return result + + raise HTTPException(status_code=30000, detail="종목을 찾을 수 없습니다.") + + def _extract_ticker_from_query(self, query:str): + """ + COMPANY_MAP 안에서 일치하는 회사 찾기 + """ + + query = query.lower() + + for ticker, info in COMPANY_MAP.items(): + for alias in info["aliases"]: + if alias.lower() in query: + return TickerInfo(ticker = ticker, company_name = info["company_name"]) + + def _extract_company_keyword(self, query:str): + """ + query : 엔비디아 분석해줘 + => 필요없는 문장 제거한 후 키워드만 추출 + """ + + stop_words = [ + "분석해줘", "분석해", "분석", "해줘", "해", "주가", "전망", "재무", "알려줘", "어때", "어떻게", "" + ] + + keyword = query + + for word in stop_words: + keyword = keyword.replace(word, "") + + return keyword.strip() + + async def _search_yahoo_symbol(self, keyword: str): + """ + yahooquery 이용 + """ + try: + result = yq.search(keyword, first_quote=True) + + if not result or (isinstance(result, dict) and "explanation" in result): + return None + + return TickerInfo(ticker=result["symbol"], company_name=result["longname"]) + + except Exception: + return None \ No newline at end of file diff --git a/project/STOCK_APP/backend/services/technical_service.py b/project/STOCK_APP/backend/services/technical_service.py new file mode 100644 index 0000000..71ac363 --- /dev/null +++ b/project/STOCK_APP/backend/services/technical_service.py @@ -0,0 +1,36 @@ +import yahooquery as yf +from ta.trend import MACD, SMAIndicator +from ta.momentum import RSIIndicator + +async def get_technical_info(ticker: str): + """ + yfinance, ta api 이용 + """ + + # 주가 데이터 다운로드 + df = yf.download(ticker, period="1y", interval="1d") + # 종가 가져오기 + close = df['Close'].squeeze() + + macd = MACD(close) + macd_value = float(macd.macd().iloc[-1]) + signal_value = float(macd.macd_signal().iloc[-1]) + trend = "bullish" if macd_value > signal_value else "bearish" + + return { + # 현재 주가 + "current_price":close.iloc[-1], + # 20 일선 + "sma20":float(SMAIndicator(close, window=20).sma_indicator().iloc[-1]), + # 60 일선 + "sma60":float(SMAIndicator(close, window=60).sma_indicator().iloc[-1]), + # rsi + "rsi":float(SMAIndicator(close, window=14).rsi().iloc[-1]), + # macd + "macd":macd.macd(), + # macd_signal + "macd_signal": signal_value, + # trend + "trend": trend, + } + diff --git a/project/STOCK_APP/map.txt b/project/STOCK_APP/map.txt new file mode 100644 index 0000000..b9f50ba --- /dev/null +++ b/project/STOCK_APP/map.txt @@ -0,0 +1,85 @@ +COMPANY_MAP = { + "NVDA": { + "company_name": "NVIDIA", + "aliases": ["엔비디아", "nvidia", "nvda"], + }, + "AAPL": { + "company_name": "Apple", + "aliases": ["애플", "apple", "aapl"], + }, + "MSFT": { + "company_name": "Microsoft", + "aliases": ["마이크로소프트", "microsoft", "msft"], + }, + "TSLA": { + "company_name": "Tesla", + "aliases": ["테슬라", "tesla", "tsla"], + }, + "GOOGL": { + "company_name": "Alphabet (Google)", + "aliases": ["구글", "google", "알파벳", "alphabet", "googl"], + }, + "AMZN": { + "company_name": "Amazon", + "aliases": ["아마존", "amazon", "amzn"], + }, + "META": { + "company_name": "Meta Platforms", + "aliases": ["메타", "meta", "페이스북", "facebook", "fb"], + }, + "AMD": { + "company_name": "Advanced Micro Devices", + "aliases": ["amd"], + }, + "INTC": { + "company_name": "Intel", + "aliases": ["인텔", "intel", "intc"], + }, + "TSM": { + "company_name": "TSMC", + "aliases": ["tsmc", "tsm", "대만반도체"], + }, + "QCOM": { + "company_name": "Qualcomm", + "aliases": ["퀄컴", "qualcomm", "qcom"], + }, + "AVGO": { + "company_name": "Broadcom", + "aliases": ["브로드컴", "broadcom", "avgo"], + }, + "MU": { + "company_name": "Micron Technology", + "aliases": ["마이크론", "micron", "mu"], + }, + "V": { + "company_name": "Visa", + "aliases": ["비자", "visa", "v"], + }, + "MA": { + "company_name": "Mastercard", + "aliases": ["마스터카드", "mastercard", "ma"], + }, + "JPM": { + "company_name": "JPMorgan Chase", + "aliases": ["jp모건", "jpmorgan", "jpm"], + }, + "JNJ": { + "company_name": "Johnson & Johnson", + "aliases": ["존슨앤존슨", "johnson", "jnj"], + }, + "MSFT": { + "company_name": "Microsoft", + "aliases": ["마이크로소프트", "microsoft", "msft"], + }, +} + + + +COMPETITOR_MAP = { + "NVDA": ["AMD", "INTC", "TSM", "AVGO"], + "AMD": ["NVDA", "INTC", "TSM"], + "TSLA": ["RIVN", "GM", "F"], + "AAPL": ["MSFT", "GOOGL", "AMZN"], + "MSFT": ["AAPL", "GOOGL", "AMZN"], + "GOOGL": ["MSFT", "META", "AMZN"], +} \ No newline at end of file