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